# Smithers documentation (full)

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

Smithers instruments and automates a code repository so changes get cheaper, faster, and smarter. Agents plan, run, and review changes through flows declared beside the code.

Open a repository, describe what you want in chat, and inspect the work in the same conversation. Files, runs, approvals, and changes appear as interactive cards. You can keep talking to Smithers while a flow runs.

[Open Smithers](https://smithers.sh/smithersai/smithers)
[Follow the quickstart](/docs/quickstart/)

![Smithers repository home with featured flows, welcome choices, chat, and the six sidebar buttons.](/images/app/home.png)

The Smithers repository opens in the app. Start with a featured flow or choose how you want to explore.

## Start with a repository

The hosted app is in private alpha for selected public repositories. Start with **smithersai/smithers**. You can explore its home, public files, and declared automation without signing in. Contributing and running flows require GitHub sign-in and alpha access.

Each repository has a **factory**: its flows, automation rules, knowledge, and environment for working on the code. The app brings those parts into one conversation.

## Use the app

  - [Quickstart](/docs/quickstart/): Explore the repository, find a flow, and prepare your first contribution.
  - [Talk to Smithers](/docs/app/chat/): Give a task, use cards, and keep the conversation going.
  - [Run a flow](/docs/app/flows/): Choose a repository flow and follow its progress.
  - [Review a change](/docs/app/changes/): Read the diff, check the evidence, and decide what to land.
  - [Explore the interface](/docs/app/): Find Wiki, Dispatcher, Flows, Secrets, History, and Account.
  - [Pricing and access](/docs/pricing/): What the free hosted alpha includes and how access works.

## Work from your own tools

Smithers also has a CLI and TypeScript libraries for defining flows and build targets beside your code. The [developer overview](/docs/developers/) leads to installation, the CLI quickstart, and the API reference.

Reading with an agent? [`/llms.txt`](/llms.txt) indexes the docs, including the app guides.

---

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

Open the Smithers repository, read its contribution guide, and find the flows that help maintain it. Then sign in to start work in a box and review the result.

The repository tour works in your browser without an account. Running flows requires GitHub sign-in and approved alpha access. You do not need to install the CLI.

## 1. Open the repository

Open [smithersai/smithers](https://smithers.sh/smithersai/smithers). This is the repository Smithers uses to build itself.

The home card shows featured flows, and the welcome card offers three ways to begin. Check that **smithersai/smithers** appears beside the message box.

![The Smithers home card shows featured flows and three choices for getting started.](/images/app/home.png)

Your starting point: the repository home, its featured flows, and the conversation.

## 2. Read the contribution guide

Select **just exploring**. The Exploring card lists the repository's guide files. Select **CONTRIBUTING.md** to open the contribution guide in chat.

Read the setup and check instructions. Use **Maximize card** if you want more room. If the preview is truncated, follow the source repository for the full file.

![The Exploring card offers README.md and CONTRIBUTING.md as actions in the chat.](/images/app/explore.png)

Read the project's own instructions before asking Smithers to change it.

You have opened a repository and read its contribution guidance without leaving the app.

## 3. Ask about the project

Enter this prompt in the message box and select **Send message**:

```text
Explain this repository. Which files should I read before making a small
documentation change? Do not modify files or start a flow.
```

Read the response and open any file cards Smithers returns. If the app requests sign-in or reports an unavailable service, complete the access step or use the guide files from step 2.

Select **Wiki** to inspect the project's notes beside the conversation. Use **Back to the conversation** to close that pane.

## 4. Find a flow

Type `/review` in the message box. The menu shows the repository's review flow and the Review command group.

Read the flow description, then press **Escape** and clear the draft. You will run a flow after connecting your account and box.

![The command menu finds the review flow by its slash name.](/images/app/slash.png)

You can discover a flow from the same message box you use to talk to Smithers.

For broader search, press **Command+K** on macOS or **Ctrl+K** on Windows and Linux. The palette lets you search by type, including files, flows, and wiki pages.

## 5. See what starts work automatically

Select **Dispatcher**. Read one event and the flows associated with it.

![Dispatcher lists the repository's declared rules, including reviews on new Changes.](/images/app/dispatcher.png)

The public Dispatcher view explains declared automation. It does not establish that those rules are running.

Select **History** to inspect the repository's mythical history. If the card says no mythical history exists, there is nothing to browse in that view. The source repository still has its Git history.

## 6. Connect your account and box

Select **Account**, then **Sign in with GitHub**. Review the permissions and complete sign-in.

![The Account action presents the next GitHub sign-in step in the conversation.](/images/app/account.png)

Sign in when you are ready to contribute. Browsing does not require an account.

Open **Account** again and check the access label. If access is not allowed, use the access-request action offered by the app. Continue once the account has access.

Type `/`, choose the **Boxes** group, and select `/workspace.open`. Select the repository and bookmark if requested. Review and approve the launch, then wait for the box to be ready.

The box gives you a working copy and a place to run the repository's tools. Its files and sessions appear under the repository in the sidebar.

## 7. Make and review a small change

Ask for a bounded task, using an issue or an improvement you found while reading:

```text
Find one unclear instruction in the contribution guide. Explain your
proposed wording and wait for me to choose it before making the edit.
```

After choosing the edit, ask Smithers to make it and run the relevant checks. Use **Flows** or the featured `/review` flow to inspect the working-copy change.

Follow the run card. You can keep chatting while it runs, and use **Steer** on the run to send a correction. If an approval appears, inspect the requested action before deciding.

![Example run card with a progress trace and controls for steering or stopping work.](/images/app/run-example.png)

Example data in the local app shows where to inspect and steer a run. Your run has its own status and output.

Open the resulting Change and read its **Diff**, **Checks**, and **Review** views. Land it only after you have reviewed the result and the repository's requirements are satisfied.

## Continue with your next task

  - [Run a flow](/docs/app/flows/): Choose a task and provide the inputs it needs.
  - [Inspect a run](/docs/app/runs/): Read its trace, send a correction, or stop work.
  - [Review a change](/docs/app/changes/): Check the diff and evidence before landing.
  - [Troubleshoot the app](/docs/app/troubleshooting/): Resolve sign-in, box, and run problems.

Prefer a terminal? Follow the separate [CLI quickstart](/docs/cli-quickstart/).

---

# Pricing
https://smithers.sh/docs/pricing/

Smithers is free to use in the hosted private alpha for selected public repositories. Start by exploring [the Smithers repository](https://smithers.sh/smithersai/smithers).

## Hosted alpha

**Free for selected public repositories.** Smithers builds these factories in public and covers their hosted inference during the alpha.

You can explore an available repository without signing in. Contributing, running flows, and using a box require GitHub sign-in and approved access. The alpha is not unrestricted self-service hosting for every repository.

To add another project, sign in with GitHub on [smithers.sh](https://smithers.sh) and install the [Smithers GitHub App](https://github.com/apps/smithers/installations/new) on the repository. Installing the app registers the repository. It does not guarantee alpha access or an opening date. See [Open a repository](/docs/app/repositories/) for the full path.

## Open-source tools

**MIT-licensed software, with no Smithers license fee.** Run the CLI and libraries in your own environment. You are responsible for your model-provider usage and the infrastructure you use.

For setup and the release candidate's supported platforms, see [CLI and libraries](/docs/developers/).

## Compare the options

Choose the option that fits where you want to work:

| Feature | Hosted alpha | Run it yourself |
| --- | --- | --- |
| Main interface | The app at smithers.sh | Your terminal and code |
| Smithers software fee | Free during the alpha | No license fee |
| Model and infrastructure costs | Covered for selected hosted alpha factories | Paid by you to your providers |
| Repository access | Selected public repositories | Repositories you can access in your environment |
| Getting started | [Open the app](https://smithers.sh/smithersai/smithers) | [Install the CLI](/docs/installation/) |

## Paid plans

Paid contributor seats and multiplayer boxes are planned. Prices, included usage, and launch dates have not been announced. There is no paid plan to choose from on this page.

The alpha's **Account** card shows identity and access. It does not offer subscription management or a usage meter.

## Access and billing questions

### Do I need my own model API key for the hosted alpha?

No. Smithers provides inference for the selected hosted alpha factories. The app does not ask you to configure a personal model API key.

### Does signing in give me alpha access?

Signing in establishes your GitHub identity. Access is a separate approval. Open **Account** to check whether your account is allowed or waiting on an access request.

### Can I use the CLI without hosted access?

Yes. The CLI and libraries run in your environment and use the credentials you configure there. Follow the [CLI quickstart](/docs/cli-quickstart/) for that path.

### Where do I manage a subscription?

The hosted alpha has no subscription-management screen. For identity, access, and sign-out, use [Account](/docs/app/account/).

---

# CLI and libraries
https://smithers.sh/docs/developers/

Use the CLI to author and run flows in your own repository. Use the TypeScript libraries when you need to define actions, build targets, and execution rules in code.

For the hosted application, start with the [app quickstart](/docs/quickstart/).

## Get started from the terminal

The 1.0 release candidate is not on npm yet. Follow the [source-checkout installation](/docs/installation/#use-the-source-checkout-before-publication), then scaffold and run your first flow. The npm command below applies after publication.

[Install the CLI](/docs/installation/)
[Read the CLI quickstart](/docs/cli-quickstart/)

```bash
npm install --global @smthrs/cli@next
```

From your project directory, scaffold a flow, edit its instructions, and configure the credential its `model:` field requires before running it:

```bash
smthrs init change
smthrs up change
```

That creates `flows/change/flow.mdx`, a flow that lives with your code, and runs it. The [CLI quickstart](/docs/cli-quickstart/) walks through the same steps with what to expect at each one.

## Explore the build graph

A workspace contains packages and their build targets. Select a file to inspect the workspace configuration or a package's test and review targets.

<HeroStage />

![Monorepo tasks materialize into a planned dependency graph, then execute: each finished step caches its outputs. Agent 1 edits ui/src/button.tsx: only the ui build and test go stale and re-run, the rest comes back from cache. When a docs:api step joins the graph, fed by core/src/parse.ts, an edit to parse.ts re-runs build, test, and docs together. The wiki and AGENTS.md sit downstream of docs, and agent 2 sits downstream of the wiki, so every change regenerates the docs the next agent works from and they never drift. A CI scene shows an issue flowing through reproduction, fix, checks, and a human approval gate into a pull request. Finally, a remote cache serves every runner: CI reads and writes it, humans and agents read it.](/images/how-smithers-works.gif)

## Supported platforms

The release candidate's required package platform is Linux with Node 22.19.0. macOS and Windows package checks are advisory and do not establish a support guarantee.

Offline Chromium tests cover the included web UI. Packaged desktop and hosted deployments require separate acceptance evidence.

## Go deeper

  - [Your first flow](/docs/tutorials/first-flow/): Build a typed flow from actions and run it durably.
  - [Markdown flows](/docs/guides/markdown-flows/): Define a flow's instructions, capabilities, and budget.
  - [CLI reference](/docs/reference/cli/): Look up commands, inputs, and outputs.
  - [MCP setup](/docs/guides/mcp-setup/): Connect Smithers to an agent through MCP.

---

# Installation
https://smithers.sh/docs/installation/

## Requirements

- Node.js 22.19+ within Node 22, or Node.js 24.11+. The CLI declares the supported range as <code>^22.19.0 || >=24.11.0</code>. The durable engine runs on Node only.
- No database server. Storage is local SQLite, and run state lives in the project's `.flows/` directory.

See the [RC support matrix](/docs/reference/support-matrix/) for exact engine ranges, tested versions, advisory platforms, and hosting limits.

## Install the CLI

The CLI installs the `smthrs` command, with `smithers` as an alias of the same executable.

### Use the source checkout before publication

These pages describe `1.0.0-rc.0`. As checked on September 4, 2026, it is not yet on npm: `@smthrs/cli` has only `latest` at `0.35.0`, and no `next` dist-tag. Use the checkout to try the version documented here.

With pnpm 11.25.0 installed:

```bash
git clone https://github.com/smithersai/smithers.git
cd smithers
pnpm install
pnpm exec smthrs --version
export PATH="$PWD/node_modules/.bin:$PATH"
```

The PATH setting makes this checkout's CLI available in the current shell. Change to your own project directory before running `smthrs init`. The source launcher uses Node's TypeScript support, so it needs no CLI build step. See [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) for repository checks.

### Install from npm after publication

Once the release candidate is published:

```bash
npm install --global @smthrs/cli@next
```

Release candidates publish to `next`, rather than `latest`. Check `smthrs --version` before following 1.0 instructions.

### Install the CLI in a target workspace

Global or source-checkout commands can initialize a project and operate its flows. Target commands load `WORKSPACE.ts` and `PACKAGE.ts`, whose imports must share the loader's physical Effect and Smithers packages. After publication, add the CLI and declarations to the project and use its local binary:

```bash
smthrs init hello
pnpm add --save-dev @smthrs/cli@next @smthrs/targets@next
pnpm exec smthrs targets
```

Matching versions in an independent global installation do not establish that shared runtime and can cause `declaration_dependency_mismatch`. Before publication, use the checkout's workspace dependencies. See [Run a target and hit the cache](/docs/tutorials/first-target/) for the complete example.

## Install the library

The npm command below applies after publication. Until then, use the packages through the checkout workspace, as the programs under `examples/` do.

A Node flow program depends on the engine packages, the Node host, and one pinned Effect:

```bash
pnpm add @smthrs/flow@next @smthrs/engine@next @smthrs/platform-node@next
```

:::caution[Effect must stay at exactly the pinned version]
A project with two Effect instances is unsupported, because schema internals are not interoperable between them.

Published Smithers libraries declare `effect` as an exact `4.0.0-rc.112` peer. Node host packages, including `@smthrs/platform-node`, also declare `@effect/platform-node` and `@effect/platform-node-shared` as exact peers at that version. Executable packages own their exact runtime dependencies. npm, Bun, and pnpm therefore install one compatible runtime without requiring an application to pin transitive implementation packages.
:::

## Verify the install

```bash
smthrs doctor
```

`doctor` runs its checks without running any flow and prints one line per check. A healthy report covers:

- registry: the flows discovered under the project's `flows/` directory.
- state and databases: the `.flows/` state directory and the migrations applied to `control.db` and `engine.db` (both report "not created yet" before the first command that needs them).
- runtime: the Node.js version against the CLI's supported range.
- jj: whether the configured Jujutsu executable is available.
- providers: which provider credentials are set, and which are exported but empty.

The command exits 1 when any check reports a failure.

## Stay current

```bash
smthrs update
```

`update` reads npm's dist-tags for `@smthrs/cli` and reports whether the installed version is current. It never installs anything. A release candidate is compared against the `next` tag first, so it is never told to downgrade to a 0.x `latest`. When a later version exists, the command prints it with the install command to run:

```text
npm install -g @smthrs/cli@<version>
```

When the install is current, it says so:

```text
@smthrs/cli 1.0.0-rc.0 is current.
```

## Next steps

  - [CLI quickstart](/docs/cli-quickstart/): Scaffold a flow and run it.
  - [CLI reference](/docs/reference/cli/): Every verb the smthrs command ships.

---

# CLI quickstart
https://smithers.sh/docs/cli-quickstart/

Create a flow that lives with your code, give it a small task, and inspect the result. To use the hosted app instead, follow the [app quickstart](/docs/quickstart/).

## 1. Install the CLI

Use Node.js 22.19+ within Node 22, or Node.js 24.11+. The durable engine runs on Node. You also need a provider credential for the model that will perform the task.

The 1.0 release candidate is not published to npm yet. Follow the [source-checkout installation](/docs/installation/#use-the-source-checkout-before-publication), including its PATH setting, then verify:

```bash
smthrs --version
```

The version should start with `1.0.`. After publication, `npm install --global @smthrs/cli@next` will install the release candidate directly.

## 2. Scaffold a flow

From the project directory where you want the agent to work, run:

```bash
smthrs init change
```

In a new workspace, this creates:

- package.json
- WORKSPACE.ts
- PACKAGE.ts
- flows/
  - change/
    - flow.mdx
- .flows/

Existing declarations and flow files are retained. In a Git or jj repository, the command adds `.flows/` to `.gitignore`. Execution databases are created when a runtime command needs them.

For an existing npm workspace, or when you only want a flow without build declarations, use `smthrs generate flow change` instead. `init` can infer a workspace toolchain from pnpm, Yarn, or Bun; it refuses an unsupported package manager before writing project files.

## 3. Give it a task and a model

Open `flows/change/flow.mdx`. Keep the frontmatter, then replace the starter body with a small, concrete task, for example:

```md
Read the project's README and package manifest. Explain how to run its tests
and name the source directories a new contributor should read first.
Do not modify files or run commands.
```

The `model:` field selects the provider and model. `init` chooses it from the first available provider credential; if none is set, the generated comment names the key required by its default model. Set that credential in your shell, or change `model:` to a seat you can use. The [init reference](/docs/reference/cli/init/) lists the selection order. A model call uses that provider's account and usage limits.

The starter declares file-read, file-write, and process-spawn capabilities for editing and testing. For this read-only task, replace its `capabilities:` line with `capabilities: ["fs:read:**"]`. Review that declaration whenever you change the task; the flow name comes from its directory, so no `name:` field is needed.

Check the configuration and flow discovery:

```bash
smthrs doctor
smthrs ls
```

`doctor` reports runtime and configuration problems without running a flow. `ls` should include `change` with the description from its frontmatter. Resolve any failing checks before launching.

## 4. Run and inspect it

```bash
smthrs up change
```

`up` plans the flow, grants its requested capabilities for this run, and launches it. Review the flow's instructions and capabilities before running it. The command stays attached until the run completes, fails, is cancelled, or waits for approval.

After it settles, list the runs and use the returned run id to inspect one:

```bash
smthrs ps
smthrs status <run-id>
smthrs logs <run-id>
```

A successful run appears as `completed`. Its transcript should contain the test instructions and source-directory explanation you requested. If it fails, `status` explains the diagnosis and `logs` shows the transcript.

Smithers records completed action results in the project's journal. A resumed run can reuse those results; starting another `up` creates a new run. See [durable execution](/docs/concepts/durable-execution/) for what replay preserves.

## Find useful flows for your project

For help choosing what to automate next, run `smthrs suggest`. It scans the project and lists candidate flows and build targets. In an interactive terminal, you can choose a suggestion for an agent to implement, then select follow-ups. `--list`, `--json`, and noninteractive sessions list suggestions without implementing them.

`suggest` chooses the first available seat in this order: a Codex subscription, Moonshot, OpenAI, Gemini, OpenRouter, then Cerebras. A Codex subscription requires an existing login; `SMITHERS_OPENAI_AUTH=chatgpt` selects that authentication mode but does not create credentials. Use `--seat provider:model` to select a seat explicitly. See the [suggest reference](/docs/reference/cli/suggest/) for setup and output details.

## Next steps

  - [Your first flow](/docs/tutorials/first-flow/): Build a typed flow from actions and run it durably.
  - [Markdown flows](/docs/guides/markdown-flows/): Define the instructions, capabilities, and budget of a flow.mdx.
  - [MCP setup](/docs/guides/mcp-setup/): Give a run's agent access to MCP tools.

---

# The interface
https://smithers.sh/docs/app/

Smithers keeps your task and its results in one conversation. The repository selector sets the context. The message box is where you ask for work, and cards show files, runs, approvals, and changes.

![The Smithers app with repository context, a home card, welcome choices, and the message box.](/images/app/home.png)

The repository name beside the message box tells you where you are working.

## The six main areas

The sidebar keeps these areas available as you work:

| Area | What you use it for |
| --- | --- |
| [Wiki](/docs/app/wiki/) | Read and edit notes about the project. |
| [Dispatcher](/docs/app/dispatcher/) | See which events are declared to start which flows. |
| [Flows](/docs/app/flows/) | Find flows and follow their runs. |
| [Secrets](/docs/app/secrets/) | Inspect the names and scope of repository secrets. |
| [History](/docs/app/history/) | Explore the repository's mythical history when it has been generated. |
| [Account](/docs/app/account/) | Check your GitHub identity and alpha access. |

Wiki and Flows open panes beside the conversation. Other results appear as cards in chat. Use **Back to the conversation** to close a pane. Use **Maximize card** when you need more room for a file or a run.

## Ask, select, or type a command

You can ask Smithers to do something, select an action on a card, or enter its slash command. For example, **Dispatcher** and `/triggers.list` open the same information.

Type `/` to browse commands. A command such as `/review` runs a featured repository flow. A dotted command such as `/runs.list` names a specific action. A trailing dot, such as `/review.`, browses that command group. The menu shows the commands available in your session.

For a command that needs more information, fill in the form Smithers presents in chat. You do not need to learn its argument syntax first.

## Boxes and sessions

A [box](/docs/app/boxes/) is the environment where the repository's code runs. Its files and sessions appear under the repository in the sidebar. Select a file to inspect it, or return to an existing terminal or agent session.

## Public browsing and signed-in work

The public repository home, guide files, and declared Dispatcher rules are available signed out. Flows, boxes, account details, and repository secrets need sign-in. A sign-in prompt is a step to complete before the requested action can run.

For the access states and what they mean, see [Account and access](/docs/app/account/).

---

# Open a repository
https://smithers.sh/docs/app/repositories/

Open a repository to give your conversation a project context. In the hosted alpha, start with [smithersai/smithers](https://smithers.sh/smithersai/smithers).

## Open an available repository

1. Visit [smithers.sh](https://smithers.sh).
2. Find the repository under **Available repos**.
3. Select **Open Smithers**.

The repository's home card shows its description and featured flows. The welcome card offers three ways to continue.

## Choose how to start

Select the option that matches your task:

| Choice | What happens next |
| --- | --- |
| **just exploring** | Shows guide files you can read, including the README and contribution guide when present. |
| **contributing to this repo** | After sign-in, offers actions to report an issue, prototype a feature request, or read the contribution guide. |
| **maintaining this repo** | After sign-in, shows repository activity and actions for issues, pull requests, runs, and triggers. |

These choices change the starting guidance. Selecting the maintainer option does not grant repository permissions.

![The Exploring card links to the Smithers README and contribution guide.](/images/app/explore.png)

Exploration uses the guide files the repository actually contains.

## Read a guide file

Select **README.md** or **CONTRIBUTING.md** on the Exploring card. The file opens in the conversation. Use **Maximize card** to read it with more room.

A file card can contain a shortened preview. If it says the file is truncated, open the source repository to read the full file.

![The contribution guide is maximized in a file card while the Smithers message box stays visible.](/images/app/file.png)

Maximize a file card to read it with more room. Select Restore to return to its embedded view.

## Change the repository context

When your account has more than one repository, use the repository selector beside the message box. Check the selected repository and box before asking for a change.

## Register another repository

The alpha opens selected public repositories. Registration takes two steps:

1. Sign in with GitHub on [smithers.sh](https://smithers.sh). On the web your GitHub sign-in is your Smithers account.
2. Install the [Smithers GitHub App](https://github.com/apps/smithers/installations/new) on the repository, or on the organization that owns it.

Installing the app registers the repository. Only repositories the app is installed on appear in the app, so a repository you can see on GitHub is not reachable here until it is installed. Registering a repository does not by itself grant alpha access or contributor permissions.

For sign-in and access approval, see [Account and access](/docs/app/account/).

---

# Talk to Smithers
https://smithers.sh/docs/app/chat/

Give Smithers a concrete outcome and the context it needs to work on the selected repository.

## Give it a task

1. Check the repository name beside the message box.
2. Write what you want to change and how you will judge the result.
3. Select **Send message**.

For a first read-only task, try this prompt:

```text
Explain this repository. Point me to the files I should read before
contributing, and explain how to run its checks. Do not change files.
```

For a change, include the behavior you see, the behavior you want, and any files or constraints you already know:

```text
The help link in the footer is hard to find. Make its label clearer,
keep the existing styling, and run the relevant checks. Show me the diff
before landing the change.
```

Smithers can answer in prose, show a card, or start a flow. If the task needs sign-in or permission, complete that step in the conversation.

## Work with cards

Cards keep the result next to the request that produced it. A file card shows the file; a run card shows progress; an approval card asks for a decision.

![An Exploring card offers README.md and CONTRIBUTING.md inside the Smithers conversation.](/images/app/explore.png)

Select a file on a card to read it without leaving the conversation.

Select **Maximize card** to inspect a larger result. Return to the conversation when you are ready to continue. A **Done** label on a listing card means the listing finished loading; inspect the run's own status to determine whether the work finished.

## Keep working while a flow runs

You can continue chatting after launching a flow. To change the instructions of a particular run, use its **Steer** control and name the correction. That directs the message to the run you are inspecting.

For progress, stopping, and resuming, see [Inspect and steer a run](/docs/app/runs/).

## Use the command menu

Type `/` in the message box to see available commands. Search by name, or select a group to explore its actions.

![The slash menu shows the repository review flow and the Review command group.](/images/app/slash.png)

The menu distinguishes a repository flow from the actions in its command group.

For finding files, notes, and commands together, see [Search the app](/docs/app/search/).

---

# Search the app
https://smithers.sh/docs/app/search/

Use the search palette when you know what you want but do not know which card or command opens it.

## Open search

1. Press **Command+K** on macOS or **Ctrl+K** on Windows and Linux.
2. Enter a name or part of a name.
3. Select a result or use the arrow keys and **Enter**.

Press **Escape** to close the palette. Type `?` in the palette to list search types and their prefixes. Typing `/` in the message box opens the flow and command menu.

![The Smithers search palette lists search types and their prefixes after entering a question mark.](/images/app/search.png)

Type ? in the palette to inspect the search types and their prefixes.

## Find a command

Type `/review` to find the repository's review flow. Select the flow to invoke it. To browse the Review group's actions, type `/review.` instead.

![Searching for /review shows a flow result and a separate Review command group.](/images/app/slash.png)

A bare flow name runs a flow; a trailing dot explores its command group.

## Search a specific area

The palette offers search types for files, flows, targets, wiki pages, history, runs, changes, issues, boxes, and secret names. Choose the relevant type to narrow the query. Secret search concerns names and metadata, not secret values.

Available results depend on your sign-in state, selected repository, and connected box. If an area says it is unavailable, follow the reason shown in the app. An unavailable search is different from a search that ran and found no matches.

For a command that requires additional fields, Smithers opens a form in the conversation. Fill in the fields and review the action before submitting it.

---

# Read and edit the Wiki
https://smithers.sh/docs/app/wiki/

Use **Wiki** to read and edit the notes Smithers keeps about the project.

## Open a note

1. Select **Wiki** in the sidebar.
2. Select a note from the list.
3. Read the note and its source and confidence information.

![The Wiki pane shows a World note, its source count, and its confidence label beside the chat.](/images/app/wiki.png)

Wiki opens beside the conversation, so you can ask about a note while reading it.

Use **Back to the conversation** to close the pane. A note's confidence label is recorded metadata; check the source when a statement matters to your task.

## Add or edit a note

Select **New note** to create a note, then write in the editor. To revise an existing note, select it and edit its content. Keep facts and their sources together so another reader can check the claim.

Editing a note is distinct from landing a code change. Review code through the [Change card](/docs/app/changes/).

## Read the repository's guide files

For the repository's README and contribution guide, return to the welcome card and select **just exploring**. The app lists the guide files it finds.

The hosted alpha can show a starter note named **World**. That note is not evidence that the repository has a generated wiki. When no generated wiki exists, the Exploring card says so and offers the repository's existing guide files.

## Inspect the factory

Select **Factory** in the Wiki pane to inspect the repository's wiki information and available environment configuration files. This is a way to read the factory's configuration. It does not create a box or regenerate the wiki.

![The Factory card shows wiki information and the repository's environment configuration files.](/images/app/factory.png)

Factory lists configuration files you can open and says when a file or generated wiki is absent.

---

# Inspect the Dispatcher
https://smithers.sh/docs/app/dispatcher/

Use **Dispatcher** to inspect a repository's declared rules and the live triggers returned by its box.

## Read the declared rules

Select **Dispatcher** in the sidebar. Each rule names an event and the work it triggers in plain language.

![The Dispatcher card lists declared events and the flows they trigger, with a Register a rule button.](/images/app/dispatcher.png)

Signed out, Dispatcher shows the rules declared by the repository.

For example, the Smithers factory declares a review when a Change opens and a review on weekday mornings. Other rules describe what to do after a landing or a box session.

The label **declared in .smithers/FACTORY.ts** identifies configuration read from the repository. It does not confirm that an event listener is active or that a flow has run.

## Inspect registered triggers

To inspect live trigger state, sign in with an approved account and connect a box. Open **Dispatcher** again. Read the status and event information returned for that box.

For a particular event, inspect the resulting [run](/docs/app/runs/) to determine whether its flow started and what happened. A declaration alone is not a run result.

## Register a rule

**Register a rule** leads to sign-in when signed out. Once signed in, it always refuses registration, even when the box's trigger service is available. For `owner/repo`, it returns:

> A rule cannot be registered on owner/repo from here yet: declare it in .smithers/FACTORY.ts, or register it with the smthrs CLI on the box.

To declare a rule, add an entry to the `on` table in `.smithers/FACTORY.ts`, such as `"schedule:0 9 * * 1-5": { flow: "lint", description: "Weekday lint" }`. Dispatcher reads declarations from the repository's `.smithers/factory.json` projection. A declaration does not register a live trigger.

To register a live cron schedule, run the CLI in the project on the box. Use `smthrs flow list` to choose an existing flow id, then register it:

```bash
smthrs triggers register nightly-lint --flow lint --cron "0 9 * * 1-5" --timezone America/New_York
```

Keep `smthrs serve` or `smthrs triggers serve` running against that project for schedules to fire. See [Triggers](/docs/guides/triggers/) for registration fields, scheduler operation, and the limits of app access.

---

# Run a flow
https://smithers.sh/docs/app/flows/

A flow is a repeatable task defined by the repository. Use one to review a change, triage an issue, or perform another task the project provides.

Running a hosted flow requires GitHub sign-in, alpha access, and an available box for a registered repository. See [Account and access](/docs/app/account/) if the app asks you to sign in or apply for access, and [Register another repository](/docs/app/repositories/#register-another-repository) if the repository you want is not in the app yet.

## Choose a flow

Open the repository home and read the flows under **Try first**. Select a featured flow, or type its slash command in the message box.

![The slash menu finds the review flow declared by the Smithers repository.](/images/app/slash.png)

Type a flow's name to find it without leaving the conversation.

The Smithers repository features these tasks:

| Flow | Task |
| --- | --- |
| `/review` | Review the working-copy change and explain the verdict. |
| `/lint` | Check the files you name against repository conventions and fix findings. |
| `/pr-triage` | Triage a GitHub pull request from its diff. |
| `/issue-triage` | Reproduce and triage a GitHub issue. |
| `/release-notes` | Draft release notes from commits since the last tag. |

The repository supplies this list, so another repository can offer different flows. You can also select **Flows** to open the flow pane after signing in.

## Supply the task

If a form appears, fill in its fields. Include the issue, files, or change the flow should work on. Read the flow's description before launching: some flows edit files, and others inspect existing work.

For a first run, use `/review` on a small change in your box. If there is no change to review, choose a task with an input you can provide.

## Start and follow the run

Submit the flow's inputs. If Smithers asks for permission, review the requested action and select **Approve** to proceed or **Deny** to decline it.

A successful launch produces a run card. Read its status and output to follow the work. A sign-in prompt or launch error means you have another step to complete; it is not a running flow.

Keep chatting while the flow runs. For its trace, steering, and controls, see [Inspect and steer a run](/docs/app/runs/). For the resulting diff, see [Review a change](/docs/app/changes/).

## Create a flow for repeated work

Ask Smithers to help define the repeated task, then review the proposed flow and its requested permissions. Flows live with the repository's code, so changes to them can go through the same review process.

To author the files directly, follow the [CLI quickstart](/docs/cli-quickstart/) or [Markdown flows](/docs/guides/markdown-flows/).

---

# Inspect and steer a run
https://smithers.sh/docs/app/runs/

A run is one execution of a flow. Open its card to see what happened and decide what to do next.

These controls require sign-in and a connected repository box.

## Open a run

After launching a flow, use the run card in the conversation. To find an earlier run, enter `/runs.list` and select the run from the list. You can also use the **view runs** action on the maintainer welcome card.

Read the run's own status. A list card marked **Done** means the list loaded; it does not mean every run in the list completed.

## Inspect the trace

The run trace shows the work as an expandable tree and a timeline. Select a row to inspect the information recorded for that step, such as inputs, output, source, or a model call's token usage.

![Example run card with status, trace filters, a call tree, and controls to stop or steer the run.](/images/app/run-example.png)

Example data in the local app: the run card keeps its trace and controls together. This is not a production run.

Use the filter controls to narrow the trace. Select **Maximize card** when the tree and details need more room. If the trace says **no journal yet**, it has no recorded steps to display.

A prototype run uses the same trace with a prototype label. Treat a prototype as evidence for a later implementation; it does not go through the normal review and landing path.

## Send a correction

Enter a correction in the run's steering input and select **Steer**. For example:

```text
Keep the existing public API. Fix the internal parser and add a regression
test for the failing input.
```

When steering is pending, the run has not consumed the correction yet. Check the following output for the requested change in direction.

## Stop or resume work

Use the run's stop action to request cancellation. Confirm the action if asked, then check the resulting run status.

For a parked run, use `/runs.resume` and supply the run shown in the app. Resuming continues that run. Starting it again creates another execution, so choose the action that matches your intent.

If the run is waiting for a decision, open its [approval](/docs/app/approvals/). If it failed, inspect its error before retrying. A disconnected box or missing credential needs a different fix from a failing test.

## Verify the result

Read the final output and any checks the flow reports. If it produced a Change, [review its diff and checks](/docs/app/changes/) before landing it. A completed run does not by itself establish that the result is ready to merge.

---

# Work in a box
https://smithers.sh/docs/app/boxes/

A box is the environment where Smithers works on a repository. It holds the working copy, terminals, agent sessions, and services for that work.

Opening a hosted box requires GitHub sign-in and access to the repository.

## Open a box

1. Select the repository you want to work on.
2. Type `/` and find the **Boxes** command group.
3. Choose its open action, `/workspace.open`.
4. Fill in the repository and bookmark if requested.
5. Review and approve the launch request.

The command keeps its `workspace.open` name; **box** is the name used in the interface. Opening a box can reuse an existing environment for that bookmark. Wait for the box's reported state before opening its terminal.

## Inspect the files

Expand the box under its repository in the sidebar. Expand a directory to load its contents, then select a file to open it as a card in the conversation.

The file tree reflects that box's working copy. Before editing, check that you selected the intended box and repository.

## Use the terminal and services

Open the box card and select its terminal action when available. The terminal runs commands in the box. Return to the conversation to ask Smithers about the output.

The box card also provides views for files, services, snapshots, and outbound network activity. The fields and controls depend on what the connected box supports. An absent or unavailable terminal is not an active session.

## Save a point to return to

Use the box's snapshot action before work you may want to revisit. Name the snapshot so its purpose is clear. After the action completes, check that the snapshot appears in the list.

Forking from a snapshot creates another box from that saved state. Review the requested action before approving it.

![Example box card with the Snapshots view selected and controls to fork, snapshot, suspend, or delete.](/images/app/box-example.png)

Example data in the local app: a named snapshot and the box's available controls. This is not a production box.

## Finish with the box

Use **Suspend** when you want to stop a box while retaining it for later work. Use **Resume** to bring it back. Deleting a box is a separate action; check the box's identity and the confirmation before deleting it.

For secrets available to box sessions, see [Repository secrets](/docs/app/secrets/). For flow execution within the box, see [Run a flow](/docs/app/flows/).

---

# Review an approval
https://smithers.sh/docs/app/approvals/

An approval card asks you to decide whether a specific action can proceed. Read what the card requests before deciding.

## Decide on a request

1. Read the requested action and the repository or run it concerns.
2. Check that the action matches the work you asked for.
3. Select **Approve** to allow it or **Deny** to decline it.

While the card says **Sending your decision**, the app is submitting the response. Wait for the accepted or denied state. If submission fails, the card shows the error; the decision has not been confirmed.

Approving a launch allows that launch. It does not mean that you have reviewed a resulting code change. Review that result separately through the [Change card](/docs/app/changes/).

## Find pending approvals

Use `/approvals.list` to open the approval inbox for your repository. Each request identifies the run it belongs to. Open the run when you need its context before deciding.

If no requests are waiting, the inbox says that no approvals are pending.

## Correct the task before approving

If the requested action is too broad or targets the wrong repository, deny it and explain the correction to Smithers. For a running flow, use [Steer](/docs/app/runs/#send-a-correction) to direct the correction to that run.

---

# Review a change
https://smithers.sh/docs/app/changes/

A **Change** is the unit of code review in Smithers Cloud. Open its card to inspect the proposed code and the evidence for landing it.

Review and landing actions require sign-in and repository permission.

## Open the Change

Open the Change from the task's result, or use `/change.view` and supply its identifier. Check the repository and revision named on the card.

## Read the diff

Start with the **Diff** view to inspect the changed files. Select **Full diff** to read the added and removed lines. If you need more room, select **Maximize card**.

When several revisions exist, use the diff's revision selectors to compare the relevant versions. A review of an earlier revision does not cover changes made after that review.

![Example Change card shows its revision, file changes, review views, and landing controls.](/images/app/change-example.png)

Example data in the local app: a documentation Change ready for inspection. This is not a production change.

## Check the evidence

Open **Checks** to inspect reported check results and **Findings** for analyzer findings. Open **Review** for review verdicts, requests, and discussion. The card can also show ownership information and a walkthrough when those are available.

![The Checks view on an example Change card shows a reported build result.](/images/app/checks-example.png)

Example data in the local app: checks belong to the revision you are inspecting. This result is illustrative.

Read the reason for a failed or missing check. Ask Smithers to address the specific finding, then inspect the updated diff and the new check result.

## Resolve review feedback

Use the actions on a review thread to respond or resolve it. If you ask for a correction, identify the file, the behavior that needs to change, and how to verify the fix.

Check the revision associated with each approval. The review strip indicates when the code has changed since a review.

## Land the result

When the checks and required reviews permit landing, use the **Land** action. Its label can include the range of changes it will land, such as **Land 1 → 2**. Check that range and read the confirmation before submitting it, then verify the landed state.

If landing is blocked, read the reason on the card and complete the missing requirement. A passing agent review does not replace a required human review.

A Change lands through Smithers Cloud. A pull request is the GitHub object used for upstream review. The repository's policy determines how work reaches GitHub; the terms refer to different steps.

For the runtime evidence behind a result, see [Inspect and steer a run](/docs/app/runs/).

---

# Repository secrets
https://smithers.sh/docs/app/secrets/

Use **Secrets** to inspect which repository secrets are available to sessions in the selected repository. Sign in with an account that has permission to read this information.

## Inspect the secret list

Select **Secrets** in the sidebar. The card identifies the repository and lists the metadata returned for each secret.

The columns describe the following:

| Column | Meaning |
| --- | --- |
| **Name** | The secret's identifier. |
| **Bound to** | The hosts covered by its binding, or **setup only** when no host binding is listed. |
| **Header** | The request headers associated with the binding. |
| **Updated** | The recorded update date, when available. |

The card does not display secret values. The scope statement says that every session in the repository may use these repository secrets.

## Understand an empty or unavailable list

**No secrets yet** means the returned list is empty. A sign-in prompt or permission error means the list was not available to your session.

## Configure a required secret

The hosted alpha's Secrets card is read-only. Ask the repository operator to configure a missing secret through the repository's supported secret-management path. Do not paste a secret value into chat to work around a missing control.

Personal secret management and a secret editor are not available in this card. For model credentials used by your local CLI, see [Model seats](/docs/guides/model-seats/).

---

# Explore History
https://smithers.sh/docs/app/history/

**History** opens the repository's **mythical history**: a narrative of how the code fits together, organized into smaller changes. It is separate from the list of flow runs.

## Open History

Select **History** in the sidebar. Read the history returned for the repository, or the empty-state explanation when none exists.

![The History card explains that the Smithers repository has no mythical history to show.](/images/app/history.png)

An empty History card means there is no mythical history to display. It does not imply that the repository has no Git commits.

The mythical history is an alpha feature. Opening this view does not create that history, rewrite the repository, or establish that the full history-generation process is running.

## Find a different kind of history

Use the view that matches what you need to inspect:

| Question | Where to look |
| --- | --- |
| What did a flow do? | [Runs](/docs/app/runs/) and the run trace. |
| What changed between revisions? | [The Change card](/docs/app/changes/) and its revision comparisons. |
| What commits exist in the source repository? | The repository's GitHub history. |
| How is the code explained through mythical history? | **History**, when the repository provides it. |

---

# Account and access
https://smithers.sh/docs/app/account/

You can explore an available public repository without signing in. Sign in with GitHub to contribute, run flows, and use a box.

## Sign in

1. Select **Sign in with GitHub** in the app.
2. Read the requested GitHub permissions.
3. Complete the GitHub sign-in and return to Smithers.

You can also select **Account** while signed out to bring the sign-in step into the conversation.

![The Account action adds a GitHub sign-in prompt to the Smithers conversation.](/images/app/account.png)

Account shows the next sign-in step when no account is connected.

## Check your access

After signing in, select **Account**. The card identifies the connected GitHub account and its access state.

The access labels mean the following:

| Label | Meaning |
| --- | --- |
| **Allowed** | The account has alpha access. Repository permissions still apply. |
| **Requested, waiting on an answer** | Your access request has been recorded and awaits a decision. |
| **Not yet allowed** | The account is signed in but has not been granted alpha access. |

If the app offers an access request, submit it and wait for approval. Signing in and receiving alpha access are separate steps.

## Inspect account details

When available, the card shows GitHub scopes with their descriptions and the boxes the app has loaded. A missing section means that information was not supplied; it is not a statement that the account has no permissions or boxes.

The hosted alpha Account card is read-only apart from **Sign out**. It does not provide a billing, usage, or subscription-management screen. See [Pricing](/docs/pricing/) for the alpha's terms.

## Sign out

Select **Sign out** on the Account card. Check that the app returns to the signed-out state before using another account.

---

# Troubleshoot the app
https://smithers.sh/docs/app/troubleshooting/

Start with the message on the card or notification. It identifies the action that failed and the next step the app needs.

## The app keeps asking me to sign in

Open **Account** and complete **Sign in with GitHub**. Return to Smithers and check that Account identifies the connected user. If the session is signed in but work is still blocked, check the access label.

**Requested, waiting on an answer** means the access request needs approval. Repeating the sign-in step does not grant access.

## Flows or boxes are unavailable

Confirm that the correct repository is selected and that your account is allowed. A hosted flow also needs an available box. Read the launch error before retrying: a missing box, an unavailable service, and a failed flow need different fixes.

For the launch path, see [Work in a box](/docs/app/boxes/).

## A notification covers a control

Dismiss the notification with its **Dismiss** button, then retry the control. Read the error first so you can address the failed action.

## Search shows no result

Check the search type and the selected repository. A refusal or unavailable message means that search source could not answer. An empty result list means the search returned no matches.

If a slash command is not recognized, type `/` to browse the commands available in your session. Use the names in that menu.

## Wiki or History is empty

The alpha does not guarantee a generated wiki or mythical history for every repository. Use **just exploring** to read the existing guide files. Use the repository's GitHub page for its source history.

## A run stopped or is waiting

Open the run card. Read the status, output, and any pending approval. If an approval is waiting, decide on that request. If the run failed, correct the reported cause before retrying.

For sending a correction, stopping, or resuming, see [Inspect and steer a run](/docs/app/runs/).

## Report a problem

Check [Smithers status](https://status.smithers.sh) for service problems. For a reproducible app issue, include the action you selected, the visible error, the repository, and the run identifier when available. Include a screenshot with private information removed.

Use the contributor welcome card's **report an issue** action when you have access, or open an issue in the [Smithers repository](https://github.com/smithersai/smithers/issues). Keep credentials and private source code out of the report.

---

# Run a target and hit the cache
https://smithers.sh/docs/tutorials/first-target/

Build a small target graph and observe its cache behavior. You will run a JavaScript program, reuse its result, and then change a declared input to make it run again. This tutorial does not call a model.

You need the [Smithers CLI](/docs/installation/), a supported Node.js version (`^22.19.0 || >=24.11.0`), pnpm, and Git.

## Create the workspace

1. **Initialize an empty project.**

   ```bash
   mkdir cache-demo
   cd cache-demo
   git init
   smthrs init cache-demo
   pnpm add --save-dev @smthrs/cli@next @smthrs/targets@next
   mkdir src
   ```

   `init` creates workspace and package declarations, plus a starter flow. This tutorial uses the target declarations; the starter flow does not run.

   The global or source-checkout CLI is sufficient for `init`. After installing the workspace dependencies, use `pnpm exec smthrs` from this directory. Target declarations and the CLI must resolve the same physical copies of Effect and the Smithers declaration packages. A separately installed global CLI can fail with `declaration_dependency_mismatch` even when its versions match. Installing the CLI and targets together and selecting the local binary keeps that runtime shared.

2. **Create an input file.**

   ```js title="src/message.js"
   console.log("Hello from the target.")
   ```

3. **Replace the generated `PACKAGE.ts` with these targets.**

   ```ts title="PACKAGE.ts"
   import { Smithers as S } from "@smthrs/targets"

   const sources = S.Filegroup({ srcs: S.glob(["src/**"]) })
   const greeting = S.Shell.Test({
     shell: "node src/message.js",
     data: [sources]
   })

   export const Package = S.Package({ targets: { sources, greeting } })
   ```

   `sources` names the files the command reads. `greeting` is a test target that depends on those files. The root package gives it the label `//:greeting`.

## Preview the work

Plan the test without executing its command:

```bash
pnpm exec smthrs test '//:greeting' --plan
```

The plan includes the selected target, its dependencies, and cache information. If the target is missing, check that the root `PACKAGE.ts` exports `Package` and includes `greeting` in `targets`.

## Run twice

Run the test:

```bash
pnpm exec smthrs test '//:greeting'
```

The first run executes `node src/message.js` and records a successful result. Run the same command again:

```bash
pnpm exec smthrs test '//:greeting'
```

The second run reports a cache hit. The command does not need to execute because its declared inputs and execution environment have not changed.

## Change an input

Change the text in `src/message.js`:

```js title="src/message.js"
console.log("Hello after an input change.")
```

Run the target again. It executes because `sources` includes that file and `greeting` consumes `sources`. The changed file produces a different key for the work.

```bash
pnpm exec smthrs test '//:greeting'
pnpm exec smthrs explain '//:greeting'
```

`explain` shows the planned key and local cache decision. Use it when a hit or miss is unexpected.

:::caution[Declare what the command reads]
Smithers can only track inputs represented in the target declaration and its environment. A command that reads an undeclared file can appear unchanged when that file changes. Add the file to `data` or the appropriate rule input before relying on the cache.
:::

## Check the failure path

Temporarily replace `src/message.js` with `process.exit(1)`, then run the test. The target fails and the CLI returns a nonzero exit status. Restore the successful program before continuing.

To force execution for one invocation, use `--no-cache`. It bypasses cache reads and still publishes the new result; it does not delete all stored results.

## Next steps

- [Run selected and affected targets](/docs/guides/build-targets/) to narrow work by package, kind, or changed files.
- [How target caching works](/docs/concepts/target-caching/) to understand inputs, outputs, and shared cache boundaries.
- [Build your first durable flow](/docs/tutorials/first-flow/) to coordinate work that needs persistent execution state.

---

# Build your first durable flow
https://smithers.sh/docs/tutorials/first-flow/

In this tutorial you'll build the smallest complete Smithers program: one action, one flow that names it, an implementation attached as a layer, and two runs of that flow, first on the in-memory engine and then on the durable engine over a SQLite journal.

Time: about 20 minutes. You need Node.js and pnpm.

## Before you begin

- You have Node.js 22.19.0 or later. The durable engine runs on Node only.
- You have pnpm for installing packages.
- The [CLI quickstart](/docs/cli-quickstart/) is a useful warm-up, but this tutorial starts from an empty directory.

## 1. Set up the project

Create a directory and install the flow and engine packages:

```bash
mkdir first-flow && cd first-flow
pnpm init
pnpm add @smthrs/flow@next @smthrs/engine@next @smthrs/engine-store@next @smthrs/flows@next @smthrs/journal@next @smthrs/kernel@next @smthrs/run-store@next @smthrs/platform-node@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112 @effect/sql-sqlite-node@4.0.0-rc.112
```

The `@next` suffix is required while 1.0 is a release candidate: `latest` still resolves the Smithers 0.x line. Effect is pinned to exactly `4.0.0-rc.112`.

Add `"type": "module"` to the generated `package.json`, so the files in this tutorial run as ES modules. Every program here runs with `node <file>.ts`; Node 22.19.0 and later execute the TypeScript directly.

```text
You should see: pnpm writes a package.json and a lockfile and installs the
packages into node_modules. Nothing from Smithers has run yet.
```

## 2. Declare the action

An action declares one operation: a stable tag, a payload schema, and a success schema, with no code attached. Create `greeting.ts` with the imports this file needs and the action declaration:

```ts title="greeting.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"

export const Greet = Action.make("examples/Greet", {
  payload: { name: Schema.String },
  success: Schema.String
})
```

`Action.make` builds pure data that travels anywhere; the code arrives separately in part 4. [Flows, actions, and plans](/docs/concepts/flows-actions-plans/) explains why declaration and implementation are separate, and the [flow API reference](/docs/reference/api/flow/) lists every option `Action.make` accepts.

## 3. Declare the flow

A flow connects action calls into a graph. Its `body` names the action with `.call` rather than calling it, because a body is planned before it runs. Add to `greeting.ts`:

```ts title="greeting.ts"
export const Greeting = Flow.make("examples/Greeting", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})
```

`Greet.call(payload)` records one plan node and executes nothing.

## 4. Attach the implementation

`toLayer` attaches the code to the declared action, and `Interpreter.layer` turns the flow's plan into runs over the registered action implementations. Add to `greeting.ts`:

```ts title="greeting.ts"
const GreetingLayer = Layer.mergeAll(
  Greet.toLayer(({ name }) => Effect.succeed(`Hello, ${name}.`)),
  Interpreter.layer(Greeting)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  // An action dispatch is recorded under a derived step identity, so the
  // engine needs a Crypto even in memory.
  Layer.provideMerge(NodeCrypto.layer)
)
```

`FlowEngine.layerMemory` supplies an engine that keeps its state in the process: gone when the process exits, and enough for a first run.

## 5. Run on the in-memory engine

Execute the flow under an explicit execution id and print the result. Add to `greeting.ts`:

```ts title="greeting.ts"
export const main: Effect.Effect<string> = Greeting.execute(
  { name: "Ada" },
  { executionId: "greeting-ada-1" }
).pipe(
  Effect.orDie,
  Effect.provide(GreetingLayer)
)

```

`execute` fails typed when a payload does not satisfy the flow's schema; `Effect.orDie` is there because this payload is valid by construction. Create a separate entry point so importing `Greeting` in later examples does not also launch an in-memory run:

```ts title="run-memory.ts"
import { main } from "./greeting.ts"
import * as Effect from "effect/Effect"

Effect.runPromise(main).then(console.log)
```

Run the program:

```bash
node run-memory.ts
```

```text
You should see: the program prints one line, the greeting result:
Hello, Ada.
```

## 6. Run durably over SQLite

The in-memory engine forgets everything at exit. To keep the run, swap the engine for `EngineStore.layer`, the durable engine that persists every action attempt and commits each lifecycle event in the same transaction as the state transition it describes. `NodeRuntime.layer` composes it over one SQLite file with the stores and sandbox it requires. Create `durable-layer.ts`:

```ts title="durable-layer.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import { StepBoundary, WorkspaceSandbox } from "@smthrs/engine-store"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import { Jj } from "@smthrs/kernel"
import { Ownership } from "@smthrs/run-store"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { dirname } from "node:path"

// This greeting changes no files. This stub lets the storage example run
// without jj; it cannot roll back filesystem changes. For file-writing
// actions, use a real Jj host, such as NodeRuntime.layerHost.
const stubJj = Layer.succeed(
  Jj.Jj,
  Jj.make({
    snapshot: () => Effect.succeed({ changeId: "tutorial-snapshot" as never }),
    restore: () => Effect.void,
    diff: () => Effect.succeed(""),
    workspaceAdd: () => Effect.void,
    workspaceForget: () => Effect.void,
    status: () => Effect.succeed("")
  })
)

// A durable FlowEngine over the SQLite file at filename.
export const durableEngine = (filename: string, hostId: string) =>
  NodeRuntime.layer(
    {
      filename,
      workspaceRoot: dirname(filename),
      owner: { hostId },
      isAlive: Ownership.sameHostPidProbe
    },
    StepBoundary.layer,
    WorkspaceSandbox.layerFileSystem(),
    Layer.empty
  ).pipe(
    Layer.provideMerge(stubJj),
    Layer.provideMerge(NodeCrypto.layer),
    Layer.provideMerge(NodeFileSystem.layer)
  )
```

Keep this file; the later tutorials build their engines through it. Now create `run-durable.ts`, which runs the same `Greeting` flow durably and reads back the journal the run wrote:

```ts title="run-durable.ts"
import { Action, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { durableEngine } from "./durable-layer.ts"
import { Greet, Greeting } from "./greeting.ts"

const main = Effect.gen(function*() {
  const result = yield* Greeting.execute({ name: "Ada" }, { executionId: "greeting-ada-2" })
  const journal = yield* Journal.Journal
  yield* journal.flush
  const page = yield* journal.entries({ runId: "greeting-ada-2" as JournalEvent.RunId, limit: 200 })
  return { result, eventTypes: page.entries.map((entry) => entry.eventType) }
}).pipe(
  Effect.provide(
    Layer.mergeAll(
      Greet.toLayer(({ name }) => Effect.succeed(`Hello, ${name}.`)),
      Interpreter.layer(Greeting)
    ).pipe(
      Layer.provideMerge(Action.layerImplementations),
      Layer.provideMerge(durableEngine("greeting.sqlite", "tutorial-host"))
    )
  ),
  Effect.scoped,
  Effect.orDie
)

Effect.runPromise(main).then(console.log)
```

```bash
node run-durable.ts
```

Only the engine changed. The flow, the action, and the implementation are the ones from parts 2 to 4, which is the point of the authoring model: durability lives underneath the program, not inside it. [Durable execution](/docs/concepts/durable-execution/) covers what the engine records. To run the durable program again from scratch, delete `greeting.sqlite` or pick a different execution id.

```text
You should see: the program prints an object with result (Hello, Ada.) and
eventTypes, one event type per journal entry the run committed. The file
greeting.sqlite now exists on disk with the run's journal inside it.
```

## Next steps

  - [Crash and resume a run](/docs/tutorials/crash-and-resume/): Suspend a durable run, drop the engine, and resume without re-executing recorded work.
  - [Flows, actions, and plans](/docs/concepts/flows-actions-plans/): The two nouns of the authoring model and how a body becomes a run.

---

# Crash and resume a run
https://smithers.sh/docs/tutorials/crash-and-resume/

In this tutorial you'll build a run that survives a crash: a flow whose step waits on a durable deferred, a first drive that suspends and drops the engine, and a second drive that completes the wait and finishes the run without re-executing the work the first drive recorded.

Time: about 15 minutes. You need the project from the first tutorial.

## Before you begin

- You have completed [Build your first durable flow](/docs/tutorials/first-flow/).
- You have the first tutorial's project, including `durable-layer.ts` and its installed packages.
- You have Node.js 22.19.0 or later.

## 1. Declare the step and the flow

The flow reviews a document, and its one declared step does the assessing. Create `review.ts` with the imports this file needs and both declarations:

```ts title="review.ts"
import { Action, DurableDeferred, Flow, FlowRuntime, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

export const Assess = Action.make("examples/Assess", {
  payload: { document: Schema.String },
  success: Schema.String
})

export const Review = Flow.make("examples/Review", {
  payload: { document: Schema.String },
  success: Schema.String,
  body: (payload) => Assess.call(payload)
})
```

## 2. Give the step a durable wait

The crash needs something to crash in the middle of. Declare a durable deferred, then write the step's implementation so it reads the document and awaits a verdict:

```ts title="review.ts"
export const Approval = DurableDeferred.make("examples/approval", {
  success: Schema.String
})

export interface Summary {
  readonly result: string
  readonly readDispatches: number
  readonly stepEntries: number
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let readDispatches = 0
    let stepEntries = 0

    const ReadDocument = Action.make({
      name: "examples/ReadDocument",
      success: Schema.String,
      tier: "sealed",
      idempotencyKey: "examples/read-document/v1",
      execute: Effect.sync(() => {
        readDispatches += 1
        return "draft body"
      })
    })

    const assess = ({ document }: { readonly document: string }) =>
      Effect.gen(function*() {
        stepEntries += 1
        const body = yield* ReadDocument
        const verdict = yield* DurableDeferred.await(Approval)
        return `${document}:${body}:${verdict}`
      })
```

The two counters are the replay contract made visible. `ReadDocument` is a sealed action, so its recorded result replays across drives and its dispatch counter moves once. The `assess` implementation is not sealed against replay, so its counter moves once per drive. `DurableDeferred.await(Approval)` is the durable wait: a completion recorded for it survives the process that was waiting. [Durable execution](/docs/concepts/durable-execution/) develops the replay contract in full.

## 3. Suspend the run and drop the engine

Build an engine layer per drive, then run the first drive. Each drive builds its own engine over the same SQLite file, which is what a process restart looks like from the database's point of view. Add inside `main`, after `assess`:

```ts title="review.ts"
    const engine = (hostId: string) =>
      Layer.mergeAll(Assess.toLayer(assess), Interpreter.layer(Review)).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(filename, hostId))
      )

    // Phase one: run until the deferred, suspend, and drop the engine.
    yield* Effect.scoped(
      Review.execute({ document: "rfc" }, { executionId: "review-1", discard: true }).pipe(
        Effect.provide(engine("worker-a"))
      )
    )
```

`DurableDeferred.await` finds no recorded completion, so the engine suspends the run and releases its claim. With `discard: true`, `execute` returns once the suspension is durable rather than waiting out the wait. When the `Effect.scoped` block ends, the engine layer is released: that is the crash this tutorial simulates, one process boundary over one SQLite file.

## 4. Resume from durable state

To finish the run, a second drive completes the deferred and re-executes the flow under the same execution id. Add after phase one:

```ts title="review.ts"
    // Phase two: a fresh engine completes the deferred and finishes the run.
    const result = yield* Effect.scoped(
      Effect.gen(function*() {
        const flowEngine = yield* FlowRuntime.FlowRuntime
        yield* flowEngine.deferredDone(Approval, {
          flowName: Review._tag,
          executionId: "review-1",
          deferredName: Approval.name,
          exit: Exit.succeed("approved")
        })
        return yield* Review.execute({ document: "rfc" }, { executionId: "review-1" })
      }).pipe(Effect.provide(engine("worker-b")))
    )
```

`deferredDone` records the completion the first drive was waiting on. The second `execute` re-drives the run, and the re-drive replays the journal: the recorded `ReadDocument` result comes back without dispatching the action again. From the CLI, [`smthrs run <run-id> --resume`](/docs/reference/cli/run/) re-drives a parked run the same way.

## 5. Confirm the recorded step ran once

Return the summary, close out `main`, and run the program:

```ts title="review.ts"
    return { result, readDispatches, stepEntries }
  }).pipe(Effect.orDie)

Effect.runPromise(main("review.sqlite")).then(console.log)
```

```bash
node review.ts
```

The result is the one pinned by the example: the document, the body read once, and the verdict. The counters say what the crash cost: the step's implementation ran twice, and the sealed action in front of the suspension dispatched once.

```text
You should see: the program prints an object with result
"rfc:draft body:approved", readDispatches 1 (the document was read once,
in the first drive), and stepEntries 2 (the step implementation ran once
per drive).
```

## Next steps

  - [Retry a flaky action](/docs/tutorials/retry-policy/): Declare a retry policy and retry an action that fails before it succeeds.
  - [Durable execution](/docs/concepts/durable-execution/): The journal, the replay contract, and what a resume re-executes.

---

# Retry a flaky action
https://smithers.sh/docs/tutorials/retry-policy/

In this tutorial you'll build a retry around a flaky action: a `RetryPolicy` value, its backoff ladder read straight off the data, an upload that fails twice and succeeds on the third attempt, and a durable run that records every attempt.

Time: about 15 minutes. You need the project from the first tutorial.

## Before you begin

- You have completed [Crash and resume a run](/docs/tutorials/crash-and-resume/) or the [CLI quickstart](/docs/cli-quickstart/).
- You have the project from [Build your first durable flow](/docs/tutorials/first-flow/), including `durable-layer.ts` and its installed packages.
- You have Node.js 22.19.0 or later.

## 1. Declare the retry policy

A `RetryPolicy` is a plain value, so the next retry delay is derived from a persisted attempt count rather than fiber-local schedule state. Create `policy.ts`:

```ts title="policy.ts"
import { RetryPolicy } from "@smthrs/flow"

export const policy = RetryPolicy.make({
  initialMs: 100,
  factor: 2,
  maxMs: 1_000,
  maxAttempts: 4,
  nonRetryable: ["examples/Fatal"]
})
```

The fields a policy carries:

| Field | Meaning |
| --- | --- |
| `initialMs` | The delay after the first failed attempt, in milliseconds. |
| `factor` | The multiplier each failed attempt applies to the delay. |
| `maxMs` | The cap on any single delay. The delay after attempt `n` is `min(initialMs * factor^(n - 1), maxMs)`. |
| `maxAttempts` | The total number of attempts allowed. |
| `jitterRatio` | Spreads the final portion of each delay uniformly; `0` disables jitter. |
| `nonRetryable` | Error tags that stop the sequence at once, whatever the attempt. |

A policy may also declare `expirationMs`, a wall-clock budget for the whole sequence. `RetryPolicy.make` validates every bound and throws a `RangeError` naming the field that is wrong.

An action declares the policy it runs under with the `retryPolicy` option of `Action.make`; this tutorial keeps the policy as a standalone value so part 2 has something to inspect.

## 2. Read the backoff ladder

`nextDelay` and `decide` are pure functions over the policy's data, so the whole backoff ladder is inspectable without running anything. Add to `policy.ts`:

```ts title="policy.ts"
import * as Option from "effect/Option"

export const ladder: ReadonlyArray<number | null> = [1, 2, 3, 4].map((attempt) =>
  Option.getOrNull(RetryPolicy.nextDelay(policy, attempt))
)

export const fatalDecision = RetryPolicy.decide(policy, {
  attempt: 1,
  error: { _tag: "examples/Fatal" }
})

console.log(ladder, fatalDecision)
```

```bash
node policy.ts
```

The `null` at attempt 4 is the policy giving up: `maxAttempts` is 4, so the fourth attempt is the last one. A tag in `nonRetryable` short-circuits the ladder at any attempt, which is what `fatalDecision` shows.

```text
You should see: the ladder [ 100, 200, 400, null ] and the decision
{ _tag: 'GiveUp', reason: 'nonRetryable' }.
```

## 3. Declare the flaky action

The flow publishes a release, and its declared step uploads it. The upload is a sealed action whose first two attempts fail with a typed `Flaky` error. Create `publish.ts`:

```ts title="publish.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"
import { durableEngine } from "./durable-layer.ts"

class Flaky extends Schema.TaggedError<Flaky>()("examples/Flaky", {
  message: Schema.String
}) {}

export const Release = Action.make("examples/Release", {
  payload: { release: Schema.String },
  success: Schema.String
})

export const Publish = Flow.make("examples/Publish", {
  payload: { release: Schema.String },
  success: Schema.String,
  body: (payload) => Release.call(payload)
})
```

## 4. Run it and read the recorded attempts

`Action.retry` is the runtime side: it re-dispatches the action and advances `Action.CurrentAttempt`, the attempt number the durable store addresses each attempt row by. The loop lives in the step's implementation, because the implementation owns the retry loop; the plan shows one node either way. Add to `publish.ts`:

```ts title="publish.ts"
export interface Summary {
  readonly result: string
  readonly dispatches: number
  readonly attempts: ReadonlyArray<number>
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let dispatches = 0
    const attempts: Array<number> = []

    const Upload = Action.make({
      name: "examples/Upload",
      success: Schema.String,
      error: Flaky,
      tier: "sealed",
      execute: Effect.gen(function*() {
        const attempt = yield* Action.CurrentAttempt
        attempts.push(attempt)
        dispatches += 1
        return attempt < 3
          ? yield* Effect.fail(new Flaky({ message: `attempt ${attempt} lost the connection` }))
          : "uploaded"
      })
    })

    const publish = ({ release }: { readonly release: string }) =>
      Action.retry(Upload, { times: 3 }).pipe(
        Effect.map((outcome) => `${release}:${outcome}`),
        Effect.catchTag("examples/Flaky", (error) => Effect.succeed(`${release}:failed:${error.message}`))
      )

    const result = yield* Effect.scoped(
      Publish.execute({ release: "v1" }, { executionId: "publish-1" }).pipe(
        Effect.provide(
          Layer.mergeAll(Release.toLayer(publish), Interpreter.layer(Publish)).pipe(
            Layer.provideMerge(Action.layerImplementations),
            Layer.provideMerge(durableEngine(filename, "retry-worker"))
          )
        )
      )
    )

    return { result, dispatches, attempts }
  }).pipe(Effect.orDie)

Effect.runPromise(main("publish.sqlite")).then(console.log)
```

```bash
node publish.ts
```

Each dispatch reads `Action.CurrentAttempt` and records it, so `attempts` is the attempt ladder the run actually climbed: 1 and 2 failed, 3 succeeded.

When an action's retry policy gives up, the engine propagates the final declared failure so typed recovery still runs. The action span records `retry.stopReason` as `"exhausted"` when the `maxAttempts` bound is reached or `"expired"` when the `expirationMs` budget is spent, together with the one-based `retry.attempt`.

A retry policy governs ordinary typed failures only: an action implementation or adapter may explicitly fail with `InfraInterrupt` and opt into a separate `interruptRetryPolicy`; engines do not synthesize that marker from fiber interruption. [Retries](/docs/concepts/retries/) explains both paths and cancellation.

```text
You should see: the program prints an object with result "v1:uploaded",
dispatches 3, and attempts [ 1, 2, 3 ]: the first two attempts failed and
the third succeeded.
```

## Next steps

  - [Add a human approval](/docs/tutorials/human-approval/): Park a run on a person's answer, refuse one, and re-ask.
  - [Retries](/docs/concepts/retries/): Policies, attempts, cancellation, and infrastructure interrupts.

---

# Add a human approval
https://smithers.sh/docs/tutorials/human-approval/

In this tutorial you'll build a flow that asks a person before it finishes: a human task that parks the run on a confirmation, a prose answer the task refuses and re-asks within its attempt budget, and the boolean answer that completes the run.

Time: about 20 minutes. You need the project from the first tutorial.

## Before you begin

- You have completed [Retry a flaky action](/docs/tutorials/retry-policy/) or the [CLI quickstart](/docs/cli-quickstart/).
- You have the project from [Build your first durable flow](/docs/tutorials/first-flow/), including `durable-layer.ts` and its installed packages.
- You have Node.js 22.19.0 or later.

## 1. Declare a flow that asks

`HumanTask.action` is the declared `system/human-task` step: a typed question with re-asking and a deadline. The body names it with `.call` like any other action; [Flows, actions, and plans](/docs/concepts/flows-actions-plans/) covers that planning rule. Create `release.ts`:

```ts title="release.ts"
import * as DurableEngineState from "@smthrs/engine-store/DurableEngineState"
import { Action, DurableDeferred, Flow, HumanTask, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

export const Release = Flow.make("examples/Release", {
  payload: { build: Schema.String },
  success: Schema.Json,
  error: HumanTask.HumanTaskFailed,
  body: ({ build }) =>
    HumanTask.action.call({
      name: "release",
      kind: "confirm",
      prompt: `Ship ${build}?`,
      maxAttempts: 3
    })
})
```

The `kind` sets what an answer may be: `ask` takes prose, `confirm` a boolean, `select` one of `options`, and `json` a value the declared `schema` accepts. `maxAttempts` is how many answers may be refused before the task fails with `HumanTaskFailed`.

## 2. Compute the answer token

Each attempt is its own durable wait point, `WaitFor/<name>#<attempt>`, so an answer is recorded through the ordinary durable deferred path. The token that addresses one attempt is computed from outside the run, exactly as a control plane computes it: the flow, the execution id, and the attempt. Add to `release.ts`:

```ts title="release.ts"
export const tokenFor = (executionId: string, attempt: number): DurableDeferred.Token =>
  DurableDeferred.tokenFromExecutionId(HumanTask.deferred("release", attempt), {
    flow: Release,
    executionId
  })
```

`HumanTask.answer({ token, value })` records an answer against the token. A re-driven run replays every answer it already has and parks on the first attempt that has none, so a restart between the park and the answer resumes on the same token.

## 3. Ask and watch the run park

Build one engine per drive over the same SQLite file, as in [Crash and resume a run](/docs/tutorials/crash-and-resume/), and add a helper that reads back which attempt the engine says the question is open on. A human task declares its own park, so the run's waiting row carries the `approval` reason and the token of the attempt that is waiting. Add to `release.ts`:

```ts title="release.ts"
const parkedAttempt = (executionId: string, attempts: number) =>
  Effect.gen(function*() {
    const state = yield* DurableEngineState.DurableEngineState
    const row = yield* state.waiting(executionId)
    if (Option.isNone(row)) return yield* Effect.die(`the run "${executionId}" is not parked`)
    const { reason, token } = row.value
    if (reason !== "approval" || token === null) {
      return yield* Effect.die(`the run "${executionId}" is not waiting on an answer`)
    }
    for (let attempt = 1; attempt <= attempts; attempt++) {
      if (tokenFor(executionId, attempt) === token) return attempt
    }
    return yield* Effect.die(`the run "${executionId}" parked on a token no attempt owns`)
  })

export interface Summary {
  readonly result: unknown
  readonly parkedOn: ReadonlyArray<number>
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const executionId = "release-1"
    const engine = (hostId: string) =>
      Layer.mergeAll(HumanTask.layer, Interpreter.layer(Release)).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(filename, hostId))
      )
    const parkedOn: Array<number> = []

    // Phase one: the question is asked and the run parks, unanswered.
    yield* Effect.scoped(
      Effect.gen(function*() {
        yield* Release.execute({ build: "v1.4.0" }, { executionId, discard: true })
        parkedOn.push(yield* parkedAttempt(executionId, 3))
      }).pipe(Effect.provide(engine("worker-a")))
    )
```

With `discard: true`, `execute` returns once the park is durable, and the run waits at waiting-approval: its row reads parked with the `approval` waiting reason and the current attempt's token. A human task is the one wait that routinely outlives the process holding it; a reviewer answers tomorrow, and whatever asked them is long gone.

Two different waits answer to two different tools. A plan approval, or an in-run ask raised through the control plane, journals a `control.approval.requested` event whose `payload` field is the exact argument for [`smthrs approve`](/docs/reference/cli/approve/); [Plan, approve, run](/docs/guides/plan-approve-run/) walks that gate end to end. A `system/human-task` wait is different: the run parks with the `approval` waiting reason and the attempt's token, and the only thing that completes it is `HumanTask.answer` against that exact token (`packages/smithers/flows/flow/src/HumanTask.ts`). `smthrs approve` does not resolve a human-task token, which is why this tutorial answers programmatically and keeps the whole lifecycle in one file.

## 4. Refuse an answer and re-ask

A `confirm` accepts only a boolean, so a prose answer is refused. The re-drive replays the recorded answer, refuses it, and parks on the second attempt's wait point, because a durable deferred keeps its first completion and re-asking needs a wait point that has none. Add after phase one:

```ts title="release.ts"
    // Phase two: answer attempt one with prose, which a confirm cannot
    // accept. The run refuses it and parks on attempt two.
    yield* Effect.scoped(
      Effect.gen(function*() {
        yield* HumanTask.answer({ token: tokenFor(executionId, 1), value: "yes, ship it" })
        yield* Release.execute({ build: "v1.4.0" }, { executionId, discard: true })
        parkedOn.push(yield* parkedAttempt(executionId, 3))
      }).pipe(Effect.provide(engine("worker-b")))
    )
```

The refusal is durable evidence, not a log line: it is recorded as a sealed step named `HumanTask/release#1/rejected`, carrying the task, the attempt, and the reason ("The answer must be a boolean."), so a reader of the journal sees why the answer was sent back. The attempt budget is what keeps this bounded: `maxAttempts: 3` buys one more re-ask after this refusal before the task fails with `HumanTaskFailed`, code `rejected`.

## 5. Answer the open attempt and complete

The last drive answers the attempt the engine parked on and finishes the run. Add after phase two, then run the program:

```ts title="release.ts"
    // Phase three: a third process answers the attempt the engine parked on.
    const result = yield* Effect.scoped(
      Effect.gen(function*() {
        yield* HumanTask.answer({ token: tokenFor(executionId, parkedOn[1]!), value: true })
        return yield* Release.execute({ build: "v1.4.0" }, { executionId })
      }).pipe(Effect.provide(engine("worker-c")))
    )

    return { result, parkedOn }
  }).pipe(Effect.orDie)

Effect.runPromise(main("release.sqlite")).then(console.log)
```

```bash
node release.ts
```

Nothing about which attempt was open lived in the process that died: each drive read the open attempt back from the waiting row, and each answer was computed against the token durable state named.

```text
You should see: the program prints an object with result true and parkedOn
[ 1, 2 ]: the question parked on attempt 1, refused the prose answer,
parked on attempt 2, and completed when attempt 2 was answered with a
boolean.
```

## Next steps

  - [Time-travel a run](/docs/tutorials/time-travel/): Rewind a recorded run and re-drive it from an earlier step.
  - [Flows, actions, and plans](/docs/concepts/flows-actions-plans/): The declared steps, including system/human-task, that a body names.

---

# Fork and rewind a run
https://smithers.sh/docs/tutorials/time-travel/

In this tutorial you'll build a small release flow, finish a run, fork it at a journal frame so a copy can publish to a different channel, and then rewind a second run to an earlier frame and re-derive a view from what remains. It takes about 30 minutes.

Fork copies a run's history up to one frame so the copy can diverge from its parent, rewind trims the run's own history back to a frame, and compensation is the per-effect undo a rewind performs on what the trimmed suffix did. [Time travel](/docs/concepts/time-travel/) explains all three. This tutorial uses the library API to show the operation directly. For existing CLI runs, use `smthrs runs inspect/replay/fork/rewind` as described in [time travel](/docs/concepts/time-travel/). The full operation and error tables live in the [time-travel API reference](/docs/reference/api/time-travel/).

**Fork replay limitation:** copied attempt rows retain their parent digests.
Actions whose keys include the run ID execute again in the child, including
compensable and irreversible actions. An explicitly shared cache environment
can reuse eligible sealed results, but copied attempts alone do not make the
prefix replayable. Make repeated external effects idempotent before driving a
fork.

## Before you begin

- Node.js 22.19.0 or later.
- A fresh directory for the tutorial files.
- The libraries, installed in that directory:

```bash
pnpm add @smthrs/flow@next @smthrs/engine@next @smthrs/engine-store@next @smthrs/journal@next @smthrs/time-travel@next @smthrs/flows@next @smthrs/kernel@next @smthrs/run-store@next @smthrs/plan@next @smthrs/platform-node@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112 @effect/sql-sqlite-node@4.0.0-rc.112
```

You need no provider key: every step here is a plain action, and no model is called.

## 1. Finish a run

The flow tags a release and publishes the tag. The tag step is sealed: its result is recorded under a stable cache key, which is what lets a fork replay it later instead of dispatching it again.

Write three files: the engine composition every part reuses, the flow declarations, and the program that finishes the parent run.

- engine.ts
- release.ts
- parent.ts

```ts title="engine.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import { EngineStore, StepBoundary, WorkspaceSandbox } from "@smthrs/engine-store"
import { Action } from "@smthrs/flow"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import { Jj } from "@smthrs/kernel"
import { Ownership } from "@smthrs/run-store"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { dirname } from "node:path"

// A Jujutsu service that records nothing. This tutorial uses sealed actions,
// so a stub keeps the wiring honest without requiring a jj binary.
export const stubJj = Layer.succeed(
  Jj.Jj,
  Jj.make({
    snapshot: () => Effect.succeed({ changeId: "tutorial-snapshot" as never }),
    restore: () => Effect.void,
    diff: () => Effect.succeed(""),
    workspaceAdd: () => Effect.void,
    workspaceForget: () => Effect.void,
    status: () => Effect.succeed("")
  })
)

// Everything EngineStore requires, minus the engine itself, over one SQLite file.
export const requirements = (filename: string) =>
  Layer.mergeAll(StepBoundary.layer, WorkspaceSandbox.layerFileSystem(), stubJj).pipe(
    Layer.provideMerge(NodeRuntime.storage(filename)),
    Layer.provideMerge(NodeCrypto.layer),
    Layer.provideMerge(NodeFileSystem.layer)
  )

// An EngineStore over the same file, with the cache environment declared so a
// sealed identity can cross the fork boundary.
export const engineLayer = (filename: string, hostId: string) =>
  EngineStore.layer({ owner: { hostId }, journalSource: `${hostId}-engine` }).pipe(
    Layer.provideMerge(
      Layer.mergeAll(requirements(filename), Action.layerCacheEnvironment({ layers: [], capabilities: {} }))
    )
  )

// A durable FlowEngine over the same file, for the rewind in part 3.
export const durableEngine = (filename: string, hostId: string) =>
  NodeRuntime.layer(
    { filename, workspaceRoot: dirname(filename), owner: { hostId }, isAlive: Ownership.sameHostPidProbe },
    StepBoundary.layer,
    WorkspaceSandbox.layerFileSystem(),
    Layer.empty
  ).pipe(
    Layer.provideMerge(stubJj),
    Layer.provideMerge(NodeCrypto.layer),
    Layer.provideMerge(NodeFileSystem.layer)
  )
```

```ts title="release.ts"
import { Action, Flow } from "@smthrs/flow"
import { Node } from "@smthrs/plan"
import * as Schema from "effect/Schema"

// The sealed step a fork replays instead of dispatching again.
export const Tag = Action.make("release/Tag", {
  payload: {},
  success: Schema.String,
  tier: "sealed",
  idempotencyKey: "release/tag/v1"
})

export const Publish = Action.make("release/Publish", {
  payload: { tag: Schema.String },
  success: Schema.String
})

export const Ship = Flow.make("release/Ship", {
  payload: {},
  success: Schema.String,
  body: () => Tag.call({}).pipe(Node.bindPlanned((tag) => Publish.call({ tag })))
})
```

```ts title="parent.ts"
import { NodeRuntime } from "@effect/platform-node"
import { Action, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { engineLayer } from "./engine.ts"
import { Publish, Ship, Tag } from "./release.ts"

let tagDispatches = 0

const tagged = Tag.toLayer(() =>
  Effect.sync(() => {
    tagDispatches += 1
    return "v1.4.0"
  })
)

// The parent publishes to the stable channel. Part 2 attaches a different
// implementation in the fork and publishes the same tag to the beta channel.
const published = Publish.toLayer((payload) =>
  Effect.succeed(`published ${payload.tag} to the stable channel`)
)

const shipLayer = Layer.mergeAll(tagged, published, Interpreter.layer(Ship)).pipe(
  Layer.provideMerge(Action.layerImplementations)
)

const program = Effect.gen(function*() {
  const result = yield* Ship.execute({}, { executionId: "ship-1" })
  const journal = yield* Journal.Journal
  yield* journal.flush
  const page = yield* journal.entries({ runId: "ship-1" as JournalEvent.RunId, limit: 200 })
  return { result, tagDispatches, journalEntries: page.entries.length }
})

const main = program.pipe(
  Effect.provide(shipLayer.pipe(Layer.provideMerge(engineLayer("time-travel.db", "ship-parent")))),
  Effect.tap((summary) => Effect.sync(() => console.log(JSON.stringify(summary, null, 2)))),
  Effect.scoped,
  Effect.orDie
)

NodeRuntime.runMain(main)
```

Run it:

```bash
node parent.ts
```

```text
You should see: the run's result "published v1.4.0 to the stable channel",
tagDispatches 1, and the count of journal entries the run recorded. The run
ship-1 is finished, and its journal is the history the next part forks.
```

## 2. Fork the run and drive the copy

A position is a run id plus a frame `{ lineageId, seq }`. A lineage id is an opaque versioned encoding the engine mints for the run, so build it with the `FlowEngine.Lineage.root` constructor rather than spelling one out. The frame below is the committed entry right before the publish step's attempt-started record, so the fork inherits the tag and not the publish. A fork refuses a live parent; this parent is finished.

```ts title="fork.ts"
import { NodeRuntime } from "@effect/platform-node"
import { FlowEngine } from "@smthrs/engine"
import { Action, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import { SqlTimeTravelStore, TimeTravel } from "@smthrs/time-travel"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { engineLayer } from "./engine.ts"
import { Publish, Ship, Tag } from "./release.ts"

let tagDispatches = 0

const tagged = Tag.toLayer(() =>
  Effect.sync(() => {
    tagDispatches += 1
    return "v1.4.0"
  })
)

// The copy publishes to the beta channel instead.
const published = Publish.toLayer((payload) =>
  Effect.succeed(`published ${payload.tag} to the beta channel`)
)

const shipLayer = Layer.mergeAll(tagged, published, Interpreter.layer(Ship)).pipe(
  Layer.provideMerge(Action.layerImplementations)
)

// The time-travel service over the same SQLite file the engine writes.
// Building it also finishes or rolls back any rewind a crash interrupted.
const timeTravelLayer = (hostId: string) =>
  TimeTravel.layer.pipe(
    Layer.provideMerge(SqlTimeTravelStore.layer),
    Layer.provideMerge(engineLayer("time-travel.db", hostId))
  )

const forkAtPublish = Effect.gen(function*() {
  const journal = yield* Journal.Journal
  yield* journal.flush
  const page = yield* journal.entries({ runId: "ship-1" as JournalEvent.RunId, limit: 200 })
  // The frame to fork at: the committed entry right before the publish step's
  // attempt-started record.
  const publishStart = page.entries.findLastIndex((entry) => entry.eventType === "flows.engine.attempt-started")
  const seq = page.entries[Math.max(publishStart - 1, 0)]?.seq ?? 0

  const timeTravel = yield* TimeTravel
  const fork = yield* timeTravel.fork({
    runId: "ship-1",
    frame: { lineageId: FlowEngine.Lineage.root("ship-1"), seq }
  })
  return { forkRunId: fork.runId }
})

const main = Effect.gen(function*() {
  const forked = yield* Effect.scoped(
    forkAtPublish.pipe(Effect.provide(shipLayer.pipe(Layer.provideMerge(timeTravelLayer("ship-fork")))))
  )

  // A fresh engine drives the fork. The shared sealed cache replays the tag, so the
  // sealed implementation never runs in this process. The publish step is
  // past the frame, so it dispatches under the implementation attached above.
  const forkResult = yield* Effect.scoped(
    Ship.execute({}, { executionId: forked.forkRunId }).pipe(
      Effect.provide(shipLayer.pipe(Layer.provideMerge(engineLayer("time-travel.db", "ship-child"))))
    )
  )

  return { ...forked, forkResult, tagDispatches }
}).pipe(
  Effect.tap((summary) => Effect.sync(() => console.log(JSON.stringify(summary, null, 2)))),
  Effect.orDie
)

NodeRuntime.runMain(main)
```

Run it:

```bash
node fork.ts
```

```text
You should see: the minted fork run id, the fork's result
"published v1.4.0 to the beta channel", and tagDispatches 0. The fork replayed
the tag the parent recorded instead of dispatching it again, and the publish
step ran in the copy under its own implementation.
```

## 3. Rewind a run and re-derive a view

Rewind is the mutating half of the service, and it refuses a live run. The run below parks at a durable deferred, which releases ownership and leaves it suspended: the state a rewind can claim. The `inspect` call folds committed journal entries up to a frame through a pure reducer; it reads and never executes, so you can derive the same view before and after the trim.

```ts title="rewind.ts"
import { NodeRuntime } from "@effect/platform-node"
import { FlowEngine } from "@smthrs/engine"
import { Action, DurableDeferred, Flow, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import { SqlTimeTravelStore, TimeTravel } from "@smthrs/time-travel"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as SqlClient from "effect/unstable/sql/SqlClient"
import { durableEngine } from "./engine.ts"

export const Post = Action.make("ledger/Post", {
  payload: {},
  success: Schema.String
})

export const Books = Flow.make("ledger/Books", {
  payload: {},
  success: Schema.String,
  body: (payload) => Post.call(payload)
})

export const Settlement = DurableDeferred.make("ledger/settlement", {
  success: Schema.String
})

const Credit = Action.make({
  name: "ledger/Credit",
  success: Schema.Number,
  tier: "sealed",
  idempotencyKey: "ledger/credit/v1",
  execute: Effect.succeed(30)
})

// The run posts a credit and then parks at the deferred.
const post = () =>
  Effect.gen(function*() {
    const amount = yield* Credit
    const settlement = yield* DurableDeferred.await(Settlement)
    return `${amount}:${settlement}`
  })

// The run's root lineage, built by the constructor that mints it.
const lineageId = FlowEngine.Lineage.root("ledger-1")

const layer = Layer.mergeAll(Post.toLayer(post), Interpreter.layer(Books)).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(TimeTravel.layer),
  Layer.provideMerge(SqlTimeTravelStore.layer),
  Layer.provideMerge(durableEngine("time-travel.db", "ledger"))
)

const program = Effect.gen(function*() {
  // Drive the run until it parks at the deferred.
  yield* Books.execute({}, { executionId: "ledger-1", discard: true })

  const journal = yield* Journal.Journal
  yield* journal.flush
  const before = yield* journal.entries({ runId: "ledger-1" as JournalEvent.RunId, limit: 200 })
  // Rewind to the middle of what the run recorded.
  const seq = before.entries[Math.floor(before.entries.length / 2)]!.seq
  const position = { runId: "ledger-1", frame: { lineageId, seq } }

  // Read-only: count the action attempts the frame covers.
  const attemptsAtFrame = yield* TimeTravel.pipe(
    Effect.flatMap((timeTravel) =>
      timeTravel.inspect(position, {
        initial: 0,
        reduce: (state: number, committed) =>
          committed.eventType === "flows.engine.attempt-started" ? state + 1 : state
      })
    )
  )

  // Mutating: archive everything after the frame.
  const timeTravel = yield* TimeTravel
  const result = yield* timeTravel.rewind(position)

  // Re-derive the same view from the trimmed journal.
  const attemptsAfter = yield* timeTravel.inspect(position, {
    initial: 0,
    reduce: (state: number, committed) =>
      committed.eventType === "flows.engine.attempt-started" ? state + 1 : state
  })

  const remaining = yield* journal.entries({ runId: "ledger-1" as JournalEvent.RunId, limit: 200 })
  const sql = yield* Effect.service(SqlClient.SqlClient)
  const audits = yield* sql<{ readonly status: string }>`
    SELECT status FROM flows_time_travel_audits WHERE id = ${result.auditId}
  `

  return {
    entriesBefore: before.entries.length,
    attemptsAtFrame,
    archived: result.archive.archived,
    remainingSeqs: remaining.entries.map((committed) => committed.seq),
    attemptsAfter,
    auditStatus: audits[0]?.status ?? "missing"
  }
})

const main = program.pipe(
  Effect.provide(layer),
  Effect.tap((summary) => Effect.sync(() => console.log(JSON.stringify(summary, null, 2)))),
  Effect.scoped,
  Effect.orDie
)

NodeRuntime.runMain(main)
```

Run it:

```bash
node rewind.ts
```

```text
You should see: the attempts the frame covered, the count of archived
entries, and the sequence numbers that remain, ending at the frame. The
re-derived count matches the count taken before the rewind, and the summary
prints the audit row's status for the rewind.
```

## Next steps

  - [Build your first agent flow](/docs/tutorials/first-agent-flow/): Author a markdown flow, run it with smthrs up, and give a step tools and a sandbox.
  - [Time travel](/docs/concepts/time-travel/): Frames, lineages, and how fork, rewind, and compensation differ.

---

# Build your first agent flow
https://smithers.sh/docs/tutorials/first-agent-flow/

In this tutorial you'll build an agent flow two ways. First, a markdown flow that reviews the uncommitted change in your repository and runs on your provider key. Then, a module flow whose model-backed step gets real file tools and runs the code it writes inside a sandbox. It takes about 20 minutes.

## Before you begin

- The Smithers CLI: `npm install --global @smthrs/cli@next`. The `@next` suffix is required while 1.0 is a release candidate.
- A provider credential in the environment: `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. [Configure model seats](/docs/guides/model-seats/) explains how a seat resolves from environment keys.
- A git repository to work in. Part 2 reviews its uncommitted change.
- Node.js 22.19.0 or later for part 3.

## 1. Author a markdown flow

Scaffold the flow from the repository root:

```bash
smthrs init reviewer
```

The scaffold creates `flows/reviewer/flow.mdx`, the `.flows/` state directory, and workspace declarations when absent. To add only a flow to an existing workspace, use `smthrs generate flow reviewer`. Rewrite the flow file to this review:

```md
---
description: Reviews the uncommitted change in this repository and returns a verdict with the reasons behind it.
capabilities: ["fs:read:**", "proc:spawn:*"]
model: anthropic:claude-sonnet-4-5
---

# Review the working-copy change

Review the uncommitted change in this repository.

## Read the change

Run `git diff --stat` and then `git diff` for the paths in scope through the
`bash` tool; that is the only way to run a command here. Read enough of
each changed file to understand the change in context. Do not call other flows.

## Judge it

Look for behavior changes without a test, an error swallowed instead of
surfaced, a public export without a JSDoc block, and a name that says less
than the code does.

## Verdict

Finish with one line, `approve` or `request-changes`, followed by at most five
findings. Each finding names the file, the line, and the fix in one sentence.
```

Keep the `model:` line `smthrs init` wrote for you: it chose the seat from the first provider credential in your environment, so with `OPENAI_API_KEY` the line reads `openai:gpt-5.6-sol`. The body is one prompt. Headings organize the instructions; they do not create durable action boundaries or enforce execution order. [Markdown flows](/docs/guides/markdown-flows/) covers every field the frontmatter accepts.

```text
You should see: init reports the flow file it created, the project's state
directory, and the seat it wrote. After your edit, the flow declares the
review's description, capabilities, and model, with its instructions
organized under headings.
```

## 2. Run the flow with smthrs up

The flow reviews the uncommitted change in the repository, so make one first: edit any tracked file and leave the change uncommitted. Then launch:

```bash
smthrs up reviewer
```

`up` plans the flow, approves the plan, and launches the run in one command, staying attached until the run settles. The launch receipt's `runId` names the run. Read the verdict back, substituting that id:

```bash
smthrs output <run-id> result
```

The reserved node id `result` names the run's final assistant output.

```text
You should see: a launch receipt carrying the run id, and up stays attached
until the run reaches a terminal status. output prints the outcome line for
result and then the verdict: one line, approve or request-changes, followed
by at most five findings.
```

## 3. Give a step real tools and a sandbox

A markdown flow hands the whole task to one agent. A module flow declares each step in TypeScript: what the author declares is the seat, the system teaching, the prompt built from the step payload, and the output schema. The model answers with a fenced `cell` block, the block is evaluated inside a QuickJS sandbox with an explicit budget and no ambient host access, and the only way out is a call to a declared flow through `ctx.call`. [Agent runtime](/docs/concepts/agent-runtime/) explains the cell loop this runs in.

Install the libraries:

```bash
pnpm add @smthrs/agent@next @smthrs/flow@next @smthrs/engine@next @smthrs/capability@next @smthrs/model@next @smthrs/registry@next @smthrs/platform-node@next @smthrs/jj@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112
```

This part also needs Jujutsu (`jj`) on your PATH. Initialize a separate repository for the files the tools will edit:

```bash
jj git init sandbox-work
```

The write tool is compensable: the engine requires a snapshot boundary so a retry can restore the files it changed. This example binds a real Jujutsu snapshot service to `sandbox-work`. The scripted model only reads and writes the two paths inside that repository. A host serving arbitrary model requests should also restrict filesystem capabilities to its workspace; see [capabilities](/docs/guides/capabilities/).

Write `sandbox.ts`. The step counts the lines in a source file and writes the count to a target file, using the standard file tools:

```ts title="sandbox.ts"
import { NodeRuntime } from "@effect/platform-node"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import * as StandardFlows from "@smthrs/agent/StandardFlows"
import * as Capability from "@smthrs/capability/Capability"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Jj, type ChangeId } from "@smthrs/jj"
import * as NodeJj from "@smthrs/jj/node/NodeJj"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import * as Effect from "effect/Effect"
import type * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Path from "effect/Path"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"

// What the step must answer with. Nothing downstream parses model text.
const Tally = Schema.Struct({
  totalLines: Schema.Number,
  wrotePath: Schema.String,
  bytesWritten: Schema.Number
})

// The step. Its prompt names the two paths, which is how the model learns
// which files this task is about.
const Summarize = AgentAction.make("tutorial/SandboxSummarize", {
  payload: { source: Schema.String, target: Schema.String },
  output: Tally,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You maintain a repository. Use the file tools rather than guessing at contents."],
  prompt: ({ source, target }) =>
    `Count the lines in the source file and write the count to the target file.\nSOURCE: ${source}\nTARGET: ${target}`
})

const Audit = Flow.make("tutorial/Audit", {
  payload: { source: Schema.String, target: Schema.String },
  success: Tally,
  error: AgentAction.AgentFailure,
  body: (payload: { readonly source: string; readonly target: string }) => Summarize.call(payload)
})

const prepared: Route.PreparedRequest = {
  routeId: "tutorial",
  protocolId: "tutorial",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

// A scripted model: it reads the two paths out of the request and answers
// with one cell that uses the tools, so the run is deterministic and needs no
// API key. Point SeatResolver at a provider route and nothing above it changes.
const scripted: Model.Model = Model.make({
  stream: (request) =>
    Stream.suspend(() => {
      const asked = [
        ...request.system.map((part) => part.text),
        ...request.messages.flatMap((message) =>
          message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
        )
      ].join("\n")
      const sourcePath = /SOURCE: (.+)/.exec(asked)?.[1]?.trim() ?? ""
      const targetPath = /TARGET: (.+)/.exec(asked)?.[1]?.trim() ?? ""
      const cell = [
        `const page = await ctx.call("read", { path: ${JSON.stringify(sourcePath)} })`,
        `const written = await ctx.call("write", {`,
        `  path: ${JSON.stringify(targetPath)},`,
        "  content: String(page.totalLines) + \"\\n\"",
        "})",
        "ctx.done({",
        "  totalLines: page.totalLines,",
        "  wrotePath: written.path,",
        "  bytesWritten: written.bytesWritten",
        "})"
      ].join("\n")
      return Stream.fromIterable([
        ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
        ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
        ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
        ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
      ])
    })
})

const root = join(process.cwd(), "sandbox-work")
const source = join(root, "notes.md")
const target = join(root, "line-count.txt")

const snapshots = Layer.effect(
  FlowEngine.SnapshotBoundary,
  Effect.map(Jj, (jj) => FlowEngine.SnapshotBoundary.of({
    snapshot: () => jj.snapshot().pipe(
      Effect.map((snapshot) => snapshot.changeId),
      Effect.orDie
    ),
    restore: (snapshot) => jj.restore(snapshot as ChangeId).pipe(Effect.orDie),
    diff: (snapshot) => jj.snapshot().pipe(
      Effect.flatMap((current) => jj.diff(snapshot as ChangeId, current.changeId)),
      Effect.orDie
    )
  }))
).pipe(Layer.provide(NodeJj.layerAt(root)))

const program = Effect.gen(function*() {
  yield* Effect.sync(() => {
    mkdirSync(root, { recursive: true })
    writeFileSync(source, "alpha\nbeta\ngamma")
  })

  // The host's filesystem, handed to the bindings as a context.
  const services = yield* Effect.context<FileSystem.FileSystem | Path.Path>()

  const host = AgentAction.layerHost({
    registry: Registry.makeNoop({
      list: () => Effect.succeed([]),
      visible: () => Effect.succeed([]),
      getOption: () => Effect.succeed(Option.none())
    }),
    // The catalog the model is shown: the seven standard file capabilities,
    // over the host's real filesystem.
    flows: [StandardFlows.filesystem(services)],
    // The explicit sandbox budget every cell in this composition runs under.
    limits: { calls: 8 },
    // The standard flows declare real capabilities, so the run needs a real
    // envelope. An empty one refuses every declared capability by contract.
    capabilityEnvelope: [new Capability.CapabilityPattern({ action: "*", resource: "*" })],
    maxFrames: 3
  })

  const seats = SeatResolver.layer({
    resolve: (id) =>
      Effect.succeed(
        Seat.make({
          id,
          model: scripted,
          route: { prepare: () => Effect.succeed(prepared) },
          contextWindowTokens: 200_000
        })
      )
  })

  const stack = Layer.mergeAll(Summarize.layer, Interpreter.layer(Audit)).pipe(
    Layer.provideMerge(Layer.mergeAll(host, seats, Agent.layer)),
    // This offline run has no approved plan envelope to derive a ceiling from.
    Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerUnclassified(), Budget.layerUnbounded())),
    // The QuickJS sandbox the cell's code runs in, and the steering source it
    // drains. Both are browser-safe defaults.
    Layer.provideMerge(Agent.layerDefaults),
    Layer.provideMerge(Action.layerImplementations),
    Layer.provideMerge(FlowEngine.layerMemory),
    Layer.provideMerge(snapshots),
    Layer.provideMerge(NodeCrypto.layer)
  )

  const tally = yield* Audit.execute({ source, target }, { executionId: "audit-1" }).pipe(
    Effect.provide(stack)
  )

  return { tally, written: readFileSync(target, "utf8") }
})

const main = program.pipe(
  Effect.provide(Layer.mergeAll(NodeFileSystem.layer, Path.layer, NodeCrypto.layer)),
  Effect.tap((summary) => Effect.sync(() => console.log(JSON.stringify(summary, null, 2)))),
  Effect.scoped,
  Effect.orDie
)

NodeRuntime.runMain(main)
```

Run it. Node 22.19.0 runs a TypeScript file directly, so there is no build step:

```bash
node sandbox.ts
```

```text
You should see: the step settles with a tally holding totalLines 3, the
target path, and bytesWritten 2, and sandbox-work/line-count.txt now holds
"3". The cell read the source through the read tool and wrote the count
through the write tool, and every call ran inside the sandbox budget.
```

## Next steps

  - [Pass typed outputs between agent steps](/docs/tutorials/agent-outputs/): Declare output schemas and chain one step's typed output into the next.
  - [Agent runtime](/docs/concepts/agent-runtime/): The cell loop, ctx.call, ctx.done, and the sandbox boundary.

---

# Pass typed outputs between agent steps
https://smithers.sh/docs/tutorials/agent-outputs/

In this tutorial you'll build a two-step research-then-write flow. The first step's answer must decode against a schema you declare, and the second reads its typed fields. You will run the chain in memory, then persist it over SQLite. It takes about 15 minutes and needs no provider key: the model responses are scripted.

A declared `output` schema is rendered into the step's system teaching as JSON Schema. The model's final answer is decoded against it, with one correction re-prompt before the step fails. [Agent runtime](/docs/concepts/agent-runtime/) explains the loop.

## Before you begin

Use the project from [Build your first agent flow](/docs/tutorials/first-agent-flow/), with its library install, and a supported Node.js version (`^22.19.0 || >=24.11.0`). Add the durable engine dependencies:

```bash
pnpm add @smthrs/plan@next @smthrs/engine-store@next @smthrs/flows@next @smthrs/kernel@next @smthrs/run-store@next @effect/sql-sqlite-node@4.0.0-rc.112
```

## 1. Declare the research step's output schema

`AgentAction.make` declares a model-backed action. Its `.layer` provides the cell loop implementation. Create `research.ts` with the action, a single-step flow, and a shared scripted host. Importing this file does not start a run.

```ts title="research.ts"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { Flow } from "@smthrs/flow"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"

const ResearchResult = Schema.Struct({
  summary: Schema.String,
  keyPoints: Schema.Array(Schema.String)
})

// The research step: a model call whose answer must be a ResearchResult.
export const Research = AgentAction.make("research/Research", {
  payload: { topic: Schema.String },
  output: ResearchResult,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You are a research assistant. Provide concise, accurate summaries."],
  prompt: ({ topic }) => `Research the topic "${topic}" and report what matters about it.`
})

export const Brief = Flow.make("research/Brief", {
  payload: { topic: Schema.String },
  success: ResearchResult,
  error: AgentAction.AgentFailure,
  body: ({ topic }) => Research.call({ topic })
})

const prepared: Route.PreparedRequest = {
  routeId: "tutorial",
  protocolId: "tutorial",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

// A scripted model, so the run is deterministic and needs no API key. Point
// the resolver at a provider route and nothing above it changes.
const scripted: Model.Model = Model.make({
  stream: (request) =>
    Stream.suspend(() => {
      const asked = [
        ...request.system.map((part) => part.text),
        ...request.messages.flatMap((message) =>
          message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
        )
      ].join("\n")
      const answer = asked.includes("Write a short article")
        ? {
          article: "Durable workflows survive restarts because their steps are recorded, not remembered.",
          wordCount: 12
        }
        : {
          summary: "Durable workflows record every step so a restart resumes instead of repeating.",
          keyPoints: ["steps are journaled", "replay is deterministic"]
        }
      const cell = `ctx.done(${JSON.stringify(answer)})`
      return Stream.fromIterable([
        ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
        ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
        ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
        ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
      ])
    })
})

// The host: one registry and one sandbox budget, shared by every model-backed
// action in the composition.
export const host = AgentAction.layerHost({
  registry: Registry.makeNoop({
    list: () => Effect.succeed([]),
    visible: () => Effect.succeed([]),
    getOption: () => Effect.succeed(Option.none())
  }),
  limits: { calls: 8 },
  capabilityEnvelope: [],
  maxFrames: 4
})

export const seats = SeatResolver.layer({
  resolve: (id) =>
    Effect.succeed(
      Seat.make({
        id,
        model: scripted,
        route: { prepare: () => Effect.succeed(prepared) },
        contextWindowTokens: 200_000
      })
    )
})

export const agentHost = Layer.mergeAll(host, seats, Agent.layer).pipe(
  Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerDefault(), Budget.layerUnbounded())),
  Layer.provideMerge(Agent.layerDefaults)
)
```

Create a separate entry point that supplies the in-memory engine:

```ts title="run-research.ts"
import { NodeRuntime } from "@effect/platform-node"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { FlowEngine } from "@smthrs/engine"
import { Action, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { agentHost, Brief, Research } from "./research.ts"

const stack = Layer.mergeAll(Research.layer, Interpreter.layer(Brief)).pipe(
  Layer.provideMerge(agentHost),
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)

const main = Brief.execute({ topic: "durable workflows" }, { executionId: "brief-1" }).pipe(
  Effect.provide(stack),
  Effect.tap((brief) => Effect.sync(() => console.log(JSON.stringify(brief, null, 2)))),
  Effect.orDie
)

NodeRuntime.runMain(main)
```

```bash
node run-research.ts
```

```text
You should see: JSON with a summary string and a keyPoints array. The final
answer decoded against ResearchResult before it reached the caller.
```

## 2. Chain the typed output into the writing step

Create `article.ts`. `Node.bindPlanned` feeds the research result into `Write.call`; planning records the calls without executing them.

```ts title="article.ts"
import * as AgentAction from "@smthrs/agent/AgentAction"
import { Flow } from "@smthrs/flow"
import { Node } from "@smthrs/plan"
import * as Schema from "effect/Schema"
import { Research } from "./research.ts"

const ArticleResult = Schema.Struct({
  article: Schema.String,
  wordCount: Schema.Number
})

// The writing step, which consumes the first step's typed fields.
export const Write = AgentAction.make("research/Write", {
  payload: {
    summary: Schema.String,
    keyPoints: Schema.Array(Schema.String)
  },
  output: ArticleResult,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You are a technical writer. Write clear, engaging content."],
  prompt: ({ keyPoints, summary }) =>
    `Write a short article from this research.\n\nSummary: ${summary}\n\nKey points:\n${
      keyPoints.map((point) => `- ${point}`).join("\n")
    }`
})

export const Article = Flow.make("research/Article", {
  payload: { topic: Schema.String },
  success: ArticleResult,
  error: AgentAction.AgentFailure,
  body: ({ topic }) =>
    Research.call({ topic }).pipe(
      Node.bindPlanned((research) => Write.call({ summary: research.summary, keyPoints: research.keyPoints }))
    )
})
```

Register both actions and the new flow in `run-article.ts`:

```ts title="run-article.ts"
import { NodeRuntime } from "@effect/platform-node"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { FlowEngine } from "@smthrs/engine"
import { Action, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { agentHost, Research } from "./research.ts"

import { Article, Write } from "./article.ts"

const stack = Layer.mergeAll(Research.layer, Write.layer, Interpreter.layer(Article)).pipe(
  Layer.provideMerge(agentHost),
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)

const main = Article.execute({ topic: "durable workflows" }, { executionId: "article-1" }).pipe(
  Effect.provide(stack),
  Effect.tap((article) => Effect.sync(() => console.log(JSON.stringify(article, null, 2)))),
  Effect.orDie
)

NodeRuntime.runMain(main)
```

```bash
node run-article.ts
```

```text
You should see: JSON with article and wordCount fields. The writing step
received the research step's summary and keyPoints as typed values.
```

## 3. Persist the chain over SQLite

Create `engine.ts` and `run-durable-article.ts`. The declarations and scripted host stay the same; the entry point supplies a durable engine and closes its database at the end of the run.

```ts title="engine.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import { StepBoundary, WorkspaceSandbox } from "@smthrs/engine-store"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import { Jj } from "@smthrs/kernel"
import { Ownership } from "@smthrs/run-store"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { dirname } from "node:path"

// These scripted steps change no files. This stub provides no filesystem
// rollback; file-writing actions need a real Jj host.
const stubJj = Layer.succeed(
  Jj.Jj,
  Jj.make({
    snapshot: () => Effect.succeed({ changeId: "tutorial-snapshot" as never }),
    restore: () => Effect.void,
    diff: () => Effect.succeed(""),
    workspaceAdd: () => Effect.void,
    workspaceForget: () => Effect.void,
    status: () => Effect.succeed("")
  })
)

// A durable FlowEngine over the project's engine database.
export const durableEngine = (filename: string, hostId: string) =>
  NodeRuntime.layer(
    { filename, workspaceRoot: dirname(filename), owner: { hostId }, isAlive: Ownership.sameHostPidProbe },
    StepBoundary.layer,
    WorkspaceSandbox.layerFileSystem(),
    Layer.empty
  ).pipe(
    Layer.provideMerge(stubJj),
    Layer.provideMerge(NodeCrypto.layer),
    Layer.provideMerge(NodeFileSystem.layer)
  )
```

```ts title="run-durable-article.ts"
import { NodeRuntime } from "@effect/platform-node"
import { Action, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { agentHost, Research } from "./research.ts"

import { Article, Write } from "./article.ts"
import { durableEngine } from "./engine.ts"

const stack = Layer.mergeAll(Research.layer, Write.layer, Interpreter.layer(Article)).pipe(
  Layer.provideMerge(agentHost),
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(durableEngine("article.sqlite", "article"))
)

const main = Article.execute({ topic: "durable workflows" }, { executionId: "article-1" }).pipe(
  Effect.provide(stack),
  Effect.tap((article) => Effect.sync(() => console.log(JSON.stringify(article, null, 2)))),
  Effect.scoped,
  Effect.orDie
)

NodeRuntime.runMain(main)
```

```bash
node run-durable-article.ts
node run-durable-article.ts
```

```text
You should see: the same article JSON from each process. The second process
reads the completed article-1 result from article.sqlite.
```

To start a new durable run, choose another execution id. Keep `article.sqlite` to retain the recorded actions and their outcomes.

This program drives the engine directly. `smthrs output` reads the control plane's run list and will not find `article-1`. To inspect node outputs through that command, launch a markdown agent flow through `smthrs up` or supply an application host that integrates module execution with the control plane. See [module flows](/docs/guides/module-flows/) and the [output reference](/docs/reference/cli/output/).

## Next steps

  - [Remember facts across runs](/docs/tutorials/memory/): Record facts in one run and recall them in a later run under one memory policy.
  - [Agent runtime](/docs/concepts/agent-runtime/): How a model-backed step runs, from prompt to decoded output.

---

# Remember facts across runs
https://smithers.sh/docs/tutorials/memory/

In this tutorial you'll build a notebook step that records three facts in one run, recalls them in a later run, and then lists the namespace with the Smithers CLI. It takes about 20 minutes.

A memory policy decides which namespace a flow tree reads and writes, whether recall runs at all, the budget recall answers within, and whether writes are kept. It is attached to the flow declaration with `WithMemory.withMemory`, and every flow that declaration names inherits it. [Memory](/docs/concepts/memory/) explains the model.

## Before you begin

- The Smithers CLI and a project: run `smthrs init notes` in a fresh directory. The project gives you the `.flows/` state directory this tutorial's facts live in.
- Node.js 22.19.0 or later.
- The libraries:

```bash
pnpm add @smthrs/agent@next @smthrs/flow@next @smthrs/engine@next @smthrs/memory@next @smthrs/harness@next @smthrs/model@next @smthrs/registry@next @smthrs/database@next @smthrs/platform-node@next @smthrs/control@next @smthrs/time-travel@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112 @effect/sql-sqlite-node@4.0.0-rc.112
```

You need no provider key: the seat resolves to a scripted model, so the runs are deterministic. Point `SeatResolver` at a provider route and nothing above it changes.

## 1. Write facts in one run

The memory store is a SQL store, and this program builds it over the project's control database, `.flows/control.db`: the same structured memory a CLI-launched run uses, so what the program writes is what `smthrs memory` reads. A cell reaches the store through two ordinary flow declarations, `remember` and `recall`, bound under one policy.

Write `notebook.ts`:

```ts title="notebook.ts"
import { NodeRuntime } from "@effect/platform-node"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { DurableWriter } from "@smthrs/database"
import * as DatabaseMigrations from "@smthrs/database/Migrations"
import * as ControlMigrations from "@smthrs/control/Migrations"
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as FlowBinding from "@smthrs/harness/FlowBinding"
import * as MemoryFlows from "@smthrs/memory/Flows"
import * as MemoryStore from "@smthrs/memory/MemoryStore"
import * as MemoryMigrations from "@smthrs/memory/Migrations"
import * as Recall from "@smthrs/memory/Recall"
import * as RecallFts from "@smthrs/memory/RecallFts"
import * as WithMemory from "@smthrs/memory/WithMemory"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import * as TimeTravelMigrations from "@smthrs/time-travel/Migrations"
import type * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"

// The bank this flow tree reads and writes.
const namespace = { kind: "flow", id: "release-notes" } as const

// The policy every run carries.
const policy: WithMemory.Policy = {
  namespace,
  recall: "auto",
  maxTokens: 2048,
  retain: "on-complete"
}

// The two memory flows, bound under one policy, as a source a cell can call.
// The declaration a host binds is the policy-carrying copy withMemory
// produced, and the handler is built from that same copy.
const memorySource = (
  services: Context.Context<MemoryStore.MemoryStore | Recall.Recall>,
  policy: WithMemory.Policy
): FlowBinding.Source => {
  const remember = WithMemory.withMemory(MemoryFlows.remember, policy)
  const recall = WithMemory.withMemory(MemoryFlows.recall, policy)
  return FlowBinding.source("memory", [
    FlowBinding.provide(
      FlowBinding.make({ flow: remember, handler: MemoryFlows.handlersFor(remember).remember }),
      services
    ),
    FlowBinding.provide(
      FlowBinding.make({ flow: recall, handler: MemoryFlows.handlersFor(recall).recall }),
      services
    )
  ])
}

// What one notebook step must answer with. Nothing downstream parses prose.
const Reading = Schema.Struct({ keys: Schema.Array(Schema.String) })

const Notebook = AgentAction.make("notes/Notebook", {
  payload: { task: Schema.String, note: Schema.String },
  output: Reading,
  seat: "anthropic:claude-sonnet-4-5",
  system: [
    "You keep release notes. Write down what you learn, and look it up later rather than guessing."
  ],
  prompt: ({ note, task }) => `TASK: ${task}\nNOTE: ${note}`
})

const Keep = Flow.make("notes/Keep", {
  payload: { task: Schema.String, note: Schema.String },
  success: Reading,
  error: AgentAction.AgentFailure,
  body: (payload: { readonly task: string; readonly note: string }) => Notebook.call(payload)
})

// The facts the record run writes.
const facts: ReadonlyArray<{ readonly key: string; readonly text: string }> = [
  { key: "changelog-format", text: "Release notes group entries by package, newest first." },
  { key: "release-cadence", text: "Release notes ship with every tagged release, never between." },
  { key: "review-owner", text: "The docs owner reviews release notes before the tag is pushed." }
]

const prepared: Route.PreparedRequest = {
  routeId: "tutorial",
  protocolId: "tutorial",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

// The cell body the model writes for one task.
const cellFor = (task: string, note: string): string => {
  if (task === "record") {
    return [
      ...facts.map((fact) =>
        // The bank is empty on purpose: the policy fills it in.
        `await ctx.call("remember", { bank: "", key: ${JSON.stringify(fact.key)}, text: ${JSON.stringify(fact.text)} })`
      ),
      `ctx.done({ keys: ${JSON.stringify(facts.map((fact) => fact.key))} })`
    ].join("\n")
  }
  return [
    // No banks and no budget for the scoped read: both come from the policy.
    `const rows = await ctx.call("recall", { banks: [], query: ${JSON.stringify(note)} })`,
    "ctx.done({ keys: rows.map((row) => row.key) })"
  ].join("\n")
}

// A scripted model that reads the task out of the request and answers with
// one cell that uses the memory flows.
const scripted: Model.Model = Model.make({
  stream: (request) =>
    Stream.suspend(() => {
      const text = [
        ...request.system.map((part) => part.text),
        ...request.messages.flatMap((message) =>
          message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
        )
      ].join("\n")
      const task = /TASK: (.+)/.exec(text)?.[1]?.trim() ?? ""
      const note = /NOTE: (.+)/.exec(text)?.[1]?.trim() ?? ""
      const cell = cellFor(task, note)
      return Stream.fromIterable([
        ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
        ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
        ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
        ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
      ])
    })
})

const registry = Registry.makeNoop({
  list: () => Effect.succeed([]),
  visible: () => Effect.succeed([]),
  getOption: () => Effect.succeed(Option.none())
})

const seats = SeatResolver.layer({
  resolve: (id) =>
    Effect.succeed(
      Seat.make({
        id,
        model: scripted,
        route: { prepare: () => Effect.succeed(prepared) },
        contextWindowTokens: 200_000
      })
    )
})

// The memory store and the FTS recall service, over the project's control
// database. Apply its complete schema before building memory: migration IDs
// share one ordered ledger, so memory's later block must not skip control.
const database = Layer.provideMerge(
  Layer.mergeAll(
    DurableWriter.layer(),
    DatabaseMigrations.layer([
      ...TimeTravelMigrations.sets,
      ControlMigrations.set,
      MemoryMigrations.set
    ])
  ),
  NodeDatabase.layer({ filename: ".flows/control.db" })
)

const memoryLayer = Layer.provideMerge(RecallFts.layer, Layer.orDie(MemoryStore.layer)).pipe(
  Layer.provideMerge(database),
  Layer.provideMerge(NodeCrypto.layer)
)

// node notebook.ts record | recall
const task = process.argv[2] ?? "record"
const note = task === "record" ? "the three release-note rules" : "release notes"

const program = Effect.gen(function*() {
  const store = yield* MemoryStore.MemoryStore
  // FTS is enabled per namespace kind, by the store, once.
  yield* store.enableFts("flow")

  // The services the bindings run their handlers in.
  const services = yield* Effect.context<MemoryStore.MemoryStore | Recall.Recall>()

  const host = AgentAction.layerHost({
    registry,
    flows: [memorySource(services, policy)],
    limits: { calls: 8 },
    capabilityEnvelope: [],
    maxFrames: 2
  })

  const reading = yield* Keep.execute({ task, note }, { executionId: `notes-${task}-1` }).pipe(
    Effect.provide(
      Layer.mergeAll(Notebook.layer, Interpreter.layer(Keep)).pipe(
        Layer.provideMerge(Layer.mergeAll(host, seats, Agent.layer)),
        // This offline run has no approved plan envelope to derive a ceiling from.
        Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerUnclassified(), Budget.layerUnbounded())),
        Layer.provideMerge(Agent.layerDefaults),
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(FlowEngine.layerMemory),
        Layer.provideMerge(NodeCrypto.layer)
      )
    )
  )

  return { task, keys: reading.keys }
})

const main = program.pipe(
  Effect.provide(Layer.mergeAll(memoryLayer, NodeFileSystem.layer)),
  Effect.tap((summary) => Effect.sync(() => console.log(JSON.stringify(summary, null, 2)))),
  Effect.scoped,
  Effect.orDie
)

NodeRuntime.runMain(main)
```

Run the record task:

```bash
node notebook.ts record
```

```text
You should see: the run's answer as JSON, the three keys it recorded:
changelog-format, release-cadence, and review-owner. Each remember call wrote
one fact into the flow:release-notes namespace of the project's control
database.
```

## 2. Recall them in a later run

Run the recall task. It is a separate process and a separate run, and nothing is handed between the two but the database:

```bash
node notebook.ts recall
```

The recall cell names no banks, so the policy's namespace decides where it reads, and the policy's `maxTokens` bounds the answer. Recall here is SQLite FTS5, an index the store maintains; `enableFts` is the store's own switch, and a namespace kind it was never enabled for fails loudly instead of quietly returning nothing.

```text
You should see: the same three keys, recalled by a later run in a new
process. The facts are durable and cross-run by construction: the first run
is gone, and what it wrote remains.
```

## 3. List the namespace

Read the same namespace with the CLI. It reads and writes the same durable store a run's `memory` flow does, over the same control database:

```bash
smthrs memory list --namespace flow:release-notes
```

[Operate on memory](/docs/guides/memory-ops/) covers seeding and removing facts from the CLI, and the [smthrs memory reference](/docs/reference/cli/memory/) covers all four subcommands.

```text
You should see: one entry per fact with key, value, and updatedAtMs. The
three keys match what the first run recorded, and each value is the stored
JSON object with a content field holding the fact's text.
```

## Next steps

  - [Guides](/docs/guides/): Task-focused guides for every Smithers surface, from tools to memory operations.
  - [Memory](/docs/concepts/memory/): Namespaces, banks, policies, and the recall budget.

---

# Guides
https://smithers.sh/docs/guides/

Use these guides when you know what you want to do. For a complete worked project, start with [Build your first durable flow](/docs/tutorials/first-flow/).

## Build and cache targets

- [Run selected and affected targets](/docs/guides/build-targets/): choose work by kind, package, or changed inputs.
- [Share target results](/docs/guides/remote-cache/): configure a remote cache and verify reuse.

## Write a flow

- [Markdown flows](/docs/guides/markdown-flows/): write an agent prompt and its frontmatter in `flow.mdx`.
- [Module flows](/docs/guides/module-flows/): define a flow in TypeScript.
- [Organize flows](/docs/guides/flow-discovery/): choose directory names and find the project root.
- [Test flows](/docs/guides/testing-flows/): test implementations without calling a live model.

## Connect an agent

- [Model seats](/docs/guides/model-seats/): choose a provider and configure credentials.
- [Tools](/docs/guides/tools/): give an agent a typed tool implementation.
- [MCP setup](/docs/guides/mcp-setup/): expose Smithers commands to another agent.
- [Agent policies](/docs/guides/agent-policies/): bound an agent's work and review its decisions.
- [Sandboxes](/docs/guides/sandboxes/): choose where code executes.

## Run and inspect work

- [Plan, approve, and run](/docs/guides/plan-approve-run/): review a plan before launching it.
- [Inspect runs](/docs/guides/inspecting-runs/): find the status, events, and output of a run.
- [Steer a run](/docs/guides/steering/): send new instructions to running work.
- [Durable waits](/docs/guides/durable-waits/): wait for a person, signal, or deadline.
- [Child flows](/docs/guides/child-flows/): delegate work and control child lifetimes.

## Operate persistent runs

- [Run flows in CI](/docs/guides/ci/): choose an exit policy and retain useful failure evidence.
- [Run CI on Smithers Cloud](/docs/guides/cloud-ci/): what runs your `PACKAGE.ts` checks today, and the planned event-driven Cloud CI.
- [Define triggers](/docs/guides/triggers/): register a cron trigger that starts a flow, host the scheduler, and read the fire ledger; repository-event triggers are planned.
- [Control plane](/docs/guides/control-plane/): host runs beyond a terminal session.
- [Artifacts and cache](/docs/guides/artifacts-cache/): store output bytes and share sealed results.
- [Retention](/docs/guides/retention/): back up state and remove data you no longer need.
- [Sync followers](/docs/guides/sync-followers/): build a read-only projection of run history.
- [Memory operations](/docs/guides/memory-ops/): retain facts and maintain conversation history.

For a run that is already failing or waiting, use [Troubleshooting](/docs/troubleshooting/).

---

# Run selected and affected targets
https://smithers.sh/docs/guides/build-targets/

Use target commands to build, test, lint, and check docs across a workspace. You need a workspace declaration and targets in `PACKAGE.ts`; [Run a target and hit the cache](/docs/tutorials/first-target/) creates a small example.

## Choose the kind of work

The command selects a target kind, and the pattern selects packages or named targets:

| Command | Work selected |
| --- | --- |
| `smthrs build <pattern>` | Build targets. |
| `smthrs test <pattern>` | Test targets. |
| `smthrs lint <pattern>` | Lint and drift checks. |
| `smthrs docs <pattern>` | Documentation targets. |
| `smthrs ci <pattern>` | Build, test, lint, and docs in one graph. |
| `smthrs review <pattern>` | Review targets. |
| `smthrs run <pattern>` | Run-kind targets, including declared agent tasks or generators. |
| `smthrs target <label>` | One exact target, using its declared kind. |

`ci` excludes `run` and `review` targets. A target can participate in more than one kind; for a generator, checking for drift and applying generated output can be different modes. Preview the selected mode before execution.

Durable workflows use `smthrs flow start <flow>`. The target command `smthrs run` does not mean “launch any flow.”

## Select packages and targets

These examples assume your workspace declares the referenced packages and targets:

```bash
smthrs test '//...' --plan
smthrs test '//packages/...' --plan
smthrs test '//packages/api:test' --plan
smthrs target '//apps/site:build' --plan
```

`//...` selects the workspace, `//packages/...` selects a subtree, and `//packages/api:test` selects a named target in one package. Use `smthrs targets` to discover actual labels before copying an example into a script.

## Read the graph

Use `query` to inspect dependencies and `graph` to see their connections:

```bash
smthrs query 'deps(//apps/site:build)'
smthrs graph '//apps/site/...'
```

A missing target usually means the pattern or exported target key is wrong. Read the declaration in the package's `PACKAGE.ts`; a package directory alone does not imply a `build` or `test` target.

## Preview affected work

Before running tests selected by a change, list the selection:

```bash
smthrs affected test '//...' --list
```

By default, `affected` compares `HEAD` with the working tree, including untracked files. Use `--base` and `--head` to compare revisions, or `--files` to select from explicit changed paths. The installed command's `--help` gives the accepted input form.

Then remove `--list` to execute the selection:

```bash
smthrs affected test '//...'
```

Changes to shared configuration can select more work than one source-file edit. That is expected when the configuration affects the execution environment.

## Watch or limit work

Rerun tests when files change:

```bash
smthrs watch test '//packages/...'
```

Bound concurrent targets when tests contend for CPU or another shared resource:

```bash
smthrs test '//packages/...' --jobs 2
```

If a cache result is unexpected, use `smthrs explain <label>` before bypassing it with `--no-cache`. [Target caching](/docs/concepts/target-caching/) explains what a key includes and how to diagnose a miss.

---

# Write a markdown flow
https://smithers.sh/docs/guides/markdown-flows/

A markdown flow is one `flows/<name>/flow.mdx` file: frontmatter declares the run, and the prose body below it is the agent's instructions. This guide writes one and runs it.

## Before you start

- Install the CLI with `npm install --global @smthrs/cli@next`. Node 22.19+ within Node 22, or Node 24.11+, is required.
- Set a provider credential. `smthrs doctor` lists the keys it reads: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `CEREBRAS_API_KEY`.

## 1. Scaffold the flow

```bash
smthrs init review
```

The name is one path segment of letters, digits, `-`, and `_`; omit it to default to a slug of the project directory name. The scaffold writes `flows/review/flow.mdx`, creates the empty `.flows/` state directory the project's runs use, and in a git or jj repository appends `.flows/` to `.gitignore` once.

To add another flow, use `smthrs generate flow <name>`. `smthrs init` also initializes workspace and target declarations; use it when setting up a new workspace.

The scaffold's `model:` line comes from the first provider credential the environment sets, in the order `smthrs doctor` reports them: `ANTHROPIC_API_KEY` resolves `anthropic:claude-sonnet-4-5`, `OPENAI_API_KEY` resolves `openai:gpt-5.6-sol`, and `OPENROUTER_API_KEY` resolves `openrouter:anthropic/claude-sonnet-4.5`. Moonshot, Gemini (including `GOOGLE_API_KEY`), and Cerebras follow; the [init reference](/docs/reference/cli/init/) lists every default. With no credential set, the scaffold still declares the default and a frontmatter comment names the key to set, because a flow without a `model:` line is not launchable at all.

## 2. Set the frontmatter

A real flow's frontmatter declares the run:

```md
---
description: Reviews the uncommitted change in this repository and returns a verdict with the reasons behind it.
capabilities: ["fs:read:**", "proc:spawn:*"]
model: openai:gpt-5.6-sol
budget:
  tokens: 200000
  milliseconds: 600000
---
```

The keys:

| Key | Meaning |
| --- | --- |
| `description` | Required. Discovery refuses a markdown flow whose description is missing or empty, and warns past 1024 characters. |
| `model` | The seat the flow runs on, `<provider>:<model>`. Change the line to run somewhere else. |
| `capabilities` | A string array bounding what the agent may do. The starter explicitly requests file reads, file writes, and process spawning; narrow that list for the task. Omitting it grants the wildcard `["*"]`, with a discovery warning. |
| `budget` | Optional `tokens` and `milliseconds` ceilings, each a positive safe integer. A malformed budget is ignored with a warning rather than tightened. |
| `name` | Unnecessary for project flows, which are named by directory path. Discovery ignores this line with a warning; rename the directory to rename the flow. |

[The flow.mdx reference](/docs/reference/flow-mdx/) has the full key table.

## 3. Write the body

Everything below the frontmatter is the prompt the agent receives. Use headings to organize its instructions. Headings do not create typed action boundaries or enforce execution order on their own:

```md
## Read the change

Run `git diff --stat` and then `git diff` for the paths in scope through the
`bash` tool; that is the only way to run a command here. Read enough of
each changed file to understand the change in context. Do not call other flows.

## Verdict

Finish with one line, `approve` or `request-changes`, followed by at most five
findings. Each finding names the file, the line, and the fix in one sentence.
```

Write instructions, not narration: name the tools the agent may use, the order of the phases, and the exact shape of the final answer. A markdown flow's whole input is one `args` string, appended verbatim after the body, so say in the prose what the arguments mean ("The appended arguments may narrow the review to a path"). Files beside `flow.mdx` are available to the agent as supporting resources, read relative to the flow directory and loaded only as needed.

## 4. Run it

```bash
smthrs flow start review
```

`smthrs flow start` plans the flow, approves the plan for the run, and launches it, staying attached until the run settles and exiting with the run's terminal status. To pass arguments, supply the `args` field as JSON:

```bash
smthrs flow start review --data '{"args":"src/flows"}'
```

To iterate, edit `flow.mdx` and run `smthrs flow start` again. When a run misbehaves, `smthrs status <run-id>` diagnoses it, and `smthrs doctor` reports discovery warnings such as an unknown frontmatter key or a malformed budget.

## Verify it worked

- `smthrs flow list` prints one item per discovered flow with its `flowId` and `description`; the scaffolded flow appears under its directory name, `review`.
- `smthrs flow start review` prints an `Accepted` receipt whose `runId` names the run, and `smthrs status <run-id>` prints the run's diagnosis card.

## See also

- [Write a module flow](/docs/guides/module-flows/): the typed TypeScript authoring model, for when prose and one `args` string are not enough.
- [Build your first agent flow](/docs/tutorials/first-agent-flow/): the guided tutorial version of this path.

---

# Share target results between machines
https://smithers.sh/docs/guides/remote-cache/

Share successful target results between developer machines and CI. This guide configures the `PACKAGE.ts` target cache; [artifacts and the step cache](/docs/guides/artifacts-cache/) covers the durable engine's separate storage APIs.

## Before you start

- Run a target locally using the [cache tutorial](/docs/tutorials/first-target/).
- Obtain an HTTPS endpoint that implements the Smithers target-cache protocol and credentials authorized by that server. An arbitrary HTTP file server is insufficient.
- Use matching source, lockfile, runtime, platform, and declared environment on both machines. Different key material produces different results even when both clients reach the same cache.

## Declare the cache

In `WORKSPACE.ts` or `.smithers/WORKSPACE.ts`, keep your existing workspace settings and replace its `cache` value with this declaration:

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

const cache = S.Cache({
  directory: ".flows",
  remote: S.RemoteCache.make({
    endpoint: "https://cache.example.com",
    read: S.Secret("CACHE_READ_TOKEN"),
    write: S.Secret("CACHE_WRITE_TOKEN")
  })
})
```

Pass `cache` to the existing `S.Workspace(name, { ... })` options. Replace the example endpoint with your cache's URL. `S.Secret` names an environment variable; the declaration does not contain its value.

Use a read credential for consumers and a publication credential for trusted writers. Enforce those permissions on the server as well. A job that receives a write credential can publish results using that credential.

For a server using one credential for both operations, replace `read` and `write` with `token: S.Secret("SMITHERS_CACHE_TOKEN")`.

## Verify publication and reuse

On a trusted machine with both credentials available, execute a target that has no cached result yet:

```bash
smthrs test '//:greeting'
```

Inspect the cache server's request log to confirm it accepted publication. Local success alone does not prove the remote accepted the result: remote errors can fall back to local execution.

In a second checkout with the same inputs and runtime, configure the same endpoint and provide the read credential. Start with an empty local result cache, then run the same target:

```bash
smthrs test '//:greeting' --format json
```

The target's result should report `status: "hit"`. Confirm a successful remote lookup in the server log. Run it again to verify local reuse; the second invocation should not need to fetch that result remotely.

If the target produces files, check that its declared output files are restored too. A cached exit status is not proof that every file your application needs was declared as an output.

## Diagnose a miss

| Observation | Next check |
| --- | --- |
| No remote request | A local hit may have satisfied the target; check the selected workspace and endpoint. |
| Unauthorized request | Confirm the credential environment variable and server permission. |
| Remote miss on matching source | Compare the planned keys with `smthrs explain <label>` on both machines, including runtime and lockfile inputs. |
| Local success after a remote warning | The command completed locally; repair the remote connection before expecting shared reuse. |
| A stale result after configuration changes | Represent result-affecting environment values in the target's declaration, and declare any configuration files it reads. |

`SMITHERS_CACHE_URL` overrides the declared endpoint for a process. Check for this override when a command contacts an unexpected server. Remote errors disable that remote for the rest of the process; retry in a fresh invocation after repairing the connection.

## Keep cache inputs explicit

For a shell target, put result-affecting values in its `env` declaration and files in `data`:

```ts
const greeting = S.Shell.Test({
  shell: "node src/message.js",
  data: [S.file("src/message.js")],
  env: { GREETING_STYLE: "formal" }
})
```

The program can read `process.env.GREETING_STYLE`. Changing the declared value changes the target attributes used in its key. If the program reads a `.env` file itself, include that file as an input as well. Supplying a credential to a cache client is separate from declaring the environment of the command being cached.

Continue with [target caching](/docs/concepts/target-caching/) for key material, output restoration, and cache controls.

---

# Write a module flow
https://smithers.sh/docs/guides/module-flows/

Use a TypeScript flow when you need typed inputs, reusable action declarations, or composition beyond a prompt. A host program supplies the action implementations and engine that execute it.

For a prompt-driven agent run, use [Markdown flows](/docs/guides/markdown-flows/). For a step-by-step introduction to library wiring, use [Build your first durable flow](/docs/tutorials/first-flow/).

## Install the dependencies

You need Node.js 22.19.0 or later and a package with `"type": "module"` in its `package.json`:

```bash
pnpm add @smthrs/flow@next @smthrs/engine@next @smthrs/platform-node@next
```

The platform package declares the compatible Effect peers. Keep the resolved Effect version aligned across the project, as described in [Installation](/docs/installation/).

## Define and run the flow

Save the complete program as `greeting.ts`:

```ts title="greeting.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 Greet = Action.make("example/Greet", {
  payload: { name: Schema.String },
  success: Schema.String
})

const Greeting = Flow.make("example/Greeting", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})

const runtime = Layer.mergeAll(
  Greet.toLayer(({ name }) => Effect.succeed(`Hello, ${name}.`)),
  Interpreter.layer(Greeting)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)

const main = Greeting.execute(
  { name: "Ada" },
  { executionId: "greeting-ada-1" }
).pipe(Effect.provide(runtime))

console.log(await Effect.runPromise(main))
```

Run it with Node:

```bash
node greeting.ts
```

The program prints:

```text
Hello, Ada.
```

`Greet.call` describes a call during planning. `toLayer` supplies its implementation, and `Interpreter.layer` connects the flow to the engine. `NodeCrypto.layer` supplies key derivation even when the engine stores state only in memory.

## Choose identity and persistence

The explicit `executionId` identifies this invocation. Without one, Smithers first checks the flow's declared idempotency key, then uses the host's execution-ID source. The default mints a fresh UUID, so equal inputs start independent executions. Retain the returned ID to resume that work, or opt into `Flow.layerExecutionIds(Flow.derived)` to identify work by its flow tag and encoded payload. See [Execution IDs and ownership](/docs/concepts/ownership/).

The in-memory engine loses its state when the process exits. To resume after a restart, compose a durable engine over persistent SQLite state. The [first flow tutorial](/docs/tutorials/first-flow/#6-run-durably-over-sqlite) supplies that complete host setup.

## Keep discovery separate from execution

The project registry can read metadata from module entries under `flows/<name>/flow.ts` without executing their code. An entry appearing in the catalog does not install its action implementations into a host.

Run a module flow through the host program that provides its implementations. Do not assume that a discovered typed module runs through the prompt-flow CLI executor. [Flow discovery](/docs/guides/flow-discovery/) describes the catalog rules.

## Verify failures

An invalid payload fails schema validation. A missing action implementation produces an unresolved-action failure instead of executing an empty step. Keep those checks in the host's tests; [Test flows](/docs/guides/testing-flows/) shows how to replace external dependencies with controlled implementations.

---

# Organize flows in a project
https://smithers.sh/docs/guides/flow-discovery/

A Smithers project is `flows/**` sources plus `.flows/` run state. This guide organizes the tree so discovery finds every flow and every command agrees on the project root.

## Before you start

- Install the CLI with `npm install --global @smthrs/cli@next`.
- Scaffold once with [`smthrs init`](/docs/reference/cli/init/), which writes the first flow and ignores `.flows/`.

## 1. Lay out the project

- flows/
  - review/
    - flow.mdx
  - deploy/
    - status/
      - flow.ts
- .flows/
  - control.db
  - engine.db
  - logs/
- .gitignore

`flows/` holds the sources, one flow per directory. `.flows/` holds run state: the `control.db` and `engine.db` SQLite databases, and the `logs/` directory a detached run writes to. Keep `.flows/` out of version control; `smthrs init` appends the ignore rule once and skips the edit outside a git or jj repository. Build workspaces also use `.smithers/WORKSPACE.ts` and `PACKAGE.ts` declarations. Keep those source files in version control; they are separate from `.flows/` runtime state.

## 2. Name flows by directory path

Discovery scans the directories under `flows/` for an entry file: `flow.ts`, `flow.mdx`, or `SKILL.md`. The flow id is the directory path below `flows/`, segments joined with `/`: `flows/review/flow.mdx` is `review`, and `flows/deploy/status/flow.ts` is `deploy/status`. An entry file directly in `flows/` is refused, because a path-named source cannot contain a root-level entry, and a frontmatter `name` or a declared `Flow.make` name is ignored with a warning.

Three rules decide what counts:

- One directory holds one flow. When several entry files sit in one directory, discovery uses the first by precedence (`flow.ts`, then `flow.mdx`, then `SKILL.md`) and warns.
- Hidden directories (any segment starting with `.`), `node_modules`, and `.git` are skipped.
- A directory with no entry file contributes nothing on its own; its subdirectories are still scanned.

## 3. Read what discovery found

`smthrs flow list` prints one item per discovered flow with its `flowId` and `description`. The listing never includes the reserved `system/*` ids, and it never reports discovery problems.

`smthrs doctor` owns the warnings. Its registry check reports how many flows were discovered and how many directories were skipped with no flow body, then prints one line per discovery warning as `code: message`. When a flow you expect is missing from `smthrs flow list`, run `smthrs doctor` before editing anything.

## 4. Resolve the project root

Every command that touches durable state resolves the same root the same way:

1. An explicit `--root`, resolved against the invocation directory.
2. Otherwise the nearest ancestor that anchors a project. `.flows/` anchors on its own; `flows/` anchors only beside a `package.json`, `.git`, or `.jj`.
3. Otherwise the invocation directory itself.

The upward walk stops at the repository boundary (`.git` or `.jj`), so a command run deep in a checkout never climbs past the repository root. To act on a project from anywhere, pass `--root`:

```bash
smthrs runs list --root /path/to/project
```

## 5. Stay clear of reserved ids

Flow ids starting with `system/` are reserved for the control plane and carry no body in 1.0. Verbs that take a flow id refuse them with exit 1:

Choose a flow ID from `smthrs flow list`; planning a reserved ID returns a refusal before execution.

A `system` directory under `flows/` produces ids the verbs refuse, so name project directories anything else.

## Verify it worked

- `smthrs flow list` lists every flow under its directory-derived id.
- `smthrs doctor` prints `ok` for the registry check with the count of flows discovered, or names the skipped directories and warnings to fix.

## See also

- [Project layout reference](/docs/reference/project-layout/): the complete file and directory contract.

---

# Plan, approve, and run a flow
https://smithers.sh/docs/guides/plan-approve-run/

Review a flow's plan before launching it. If you already trust the plan and want one command to plan, approve, and launch, use `smthrs flow start <flow>`.

## Before you start

- Install the [CLI](/docs/installation/) and work from your project's root.
- Run `smthrs flow list` to find a flow ID. The examples below use `deploy/status`; replace it with your flow's ID.
- Configure any [model seat](/docs/guides/model-seats/) the flow needs.
- Install `jq` if you want to save the approval payload with the shell command below.

## Review and launch

1. **Inspect the plan.**

   ```bash
   smthrs flow plan deploy/status
   ```

   Planning returns the node graph, cache decisions, plan digest, and `approval` payload. It does not execute the flow. Supply inputs as `key=value` arguments or a `--data` JSON object.

2. **Save the exact approval payload.**

   ```bash
   smthrs flow plan deploy/status --format json > plan.json
   jq -e '.approval' plan.json > approval.json
   ```

   Read `plan.json` before proceeding. Check the operations and declared effects. `approval.json` must contain the plan's approval object, including its target, scope, and idempotency key.

3. **Approve it.**

   ```bash
   smthrs approvals approve @approval.json --scope run
   ```

   The decision binds to the submitted payload. It does not launch a run. Do not edit the payload between approval and execution; changed work needs a new plan and approval.

   For a discovered prompt flow, the plan also binds the measured prompt source and all discovered metadata, including model and parameters. The runtime checks that identity at launch and on resume. Editing the flow file or changing its model invalidates the old approval; refresh discovery, create a new plan, and review it again.

4. **Execute the approved plan.**

   ```bash
   smthrs flow execute @approval.json
   ```

   A successful admission returns an `Accepted` receipt with the run ID. A local attached invocation stays with the run until it settles or parks. A remote invocation returns its control receipt; inspect the remote run separately.

## Choose an approval scope

The explicit approval command defaults to `once`. The one-command `flow start` path grants `run` scope.

| Scope | Use it for |
| --- | --- |
| `once` | This approval request only. |
| `run` | A grant whose scope extends through the run. |
| `remembered` | A grant that can apply to matching future requests. |

The payload and scope determine what a grant covers. `remembered` does not approve arbitrary future work.

## Resume a run waiting for approval

An in-run approval request can park the execution at `waiting-approval`. Inspect the run and pending requests:

```bash
smthrs runs show <run-id>
smthrs approvals list
```

Replace `<run-id>` with the ID returned at launch. Save the exact request payload to `approval.json`, then approve it and resume that execution:

```bash
smthrs approvals approve @approval.json --scope run
smthrs runs resume <run-id>
```

Starting the flow again is a new launch path; use `runs resume` for the existing parked execution. [Waiting for approval](/docs/troubleshooting/waiting-approval/) covers missing or mismatched requests.

## Run without a separate review

To plan, approve with run scope, and launch in one command:

```bash
smthrs flow start deploy/status
```

To keep a local run alive after the terminal exits:

```bash
smthrs flow start deploy/status --detached
```

A detached launch returns the durable run ID and log path after the child acknowledges admission. It needs a local executor; `--detached` cannot launch through `--remote`.

## Verify the outcome

Use `smthrs runs show <run-id>` to read the current status and diagnosis. A local attached launch exits `0` for completion, `1` for failure, `3` for waiting on approval, or `130` for cancellation. A successful read command does not mean the run succeeded; read its `status` field.

Continue with [Inspect runs](/docs/guides/inspecting-runs/) to read events and outputs, or [Run flows in CI](/docs/guides/ci/) to choose how a pipeline handles a parked run.

---

# Cancel, signal, and steer a run
https://smithers.sh/docs/guides/steering/

Choose the command by the change you need: `cancel` stops the run, `signal` delivers durable JSON the flow waits on, and `steer` sends the agent an attributed text message.

## Before you start

- The run id, from the launch receipt or `smthrs runs list`.
- What the run is doing: `smthrs runs show <run-id>` shows its status and what it waits for.

## 1. Choose the verb

| To... | Use |
| --- | --- |
| End one run, including one parked at `waiting-approval` | [`smthrs runs cancel`](/docs/reference/cli/cancel/) |
| Deliver a JSON value a named wait consumes | [`smthrs runs signal`](/docs/reference/cli/signal/) |
| Redirect the agent mid-run with a text message | [`smthrs runs steer`](/docs/reference/cli/steer/) |
| End every non-terminal run in the project | [`smthrs runs cancel-all`](/docs/reference/cli/down/) |

A signal and a steering message are not interchangeable. A signal completes a wait point the flow declared by name; a flow with no matching wait cannot take it. A steering message reaches the agent at its next turn boundary whether or not the run parked. [Durable waits](/docs/guides/durable-waits/) covers the wait-point model.

## 2. Signal a named wait

```bash
smthrs runs signal <run-id> '{"name":"shipped","payload":{"approved":true}}'
```

The signal is one JSON argument with a string `name` and any JSON `payload`. Delivery is durable and idempotent: the delivery key combines the run id with a digest of the canonical payload, so re-sending the identical signal replays the original receipt (`AlreadyApplied`) rather than delivering twice.

Match the flow's declared wait. A run parked on a differently named wait fails with exit 1 and a `no_matching_wait` error that points at `smthrs runs show`; a settled run answers with a `Terminal` receipt instead.

## 3. Steer the agent

```bash
smthrs runs steer <run-id> --message "Skip the benchmark suite; run the unit tests only."
```

The message is durable, attributed to the operator principal `cli` with a timestamp, and delivered to the run at its next turn boundary. Each invocation mints a fresh message id, so two invocations deliver two messages rather than one replayed receipt. Steering never takes a run over: `--takeover` was removed in 1.0 and exits 1 pointing at the migration guide.

## 4. Cancel a run

```bash
smthrs runs cancel <run-id>
```

Cancellation is durable on the run row and interrupts the run through the control plane. Attached child work follows the parent cancellation scope. Detached children have a separate lifetime; see [Child flows](/docs/guides/child-flows/) before assuming a parent cancellation stops them. A run that already settled answers with its own `Terminal` receipt, so repeating a cancel is safe.

The exit code follows the run's terminal status, because a cancel is an interruption rather than a failure of the command: 130 when the run is `cancelled`, 0 when it had already completed, 1 when it had already failed. Cancelling never deletes the run row or its journal; [`smthrs gc`](/docs/reference/cli/gc/) does that.

## 5. Cancel everything non-terminal

```bash
smthrs runs cancel-all
```

`smthrs runs cancel-all` cancels every run in the project that has not reached a terminal status: `accepted`, `running`, `parked`, and `waiting-approval` runs each get one cancel, while `completed`, `failed`, and `cancelled` runs are left as they are. It prints one `cancelled` entry per run it cancelled; a project with no non-terminal runs prints an empty list and exits 0.

## Verify it worked

- `smthrs runs show <run-id>` shows the result: `cancelled` after a cancel, or the run moving again after a signal completed the wait it parked on.
- The receipts say what happened: `Accepted` on first delivery, `AlreadyApplied` on an identical resend, `Terminal` once the run had settled.

## See also

- [Durable waits](/docs/guides/durable-waits/): how a flow parks on a named wait and what a signal completes.

---

# Inspect runs
https://smithers.sh/docs/guides/inspecting-runs/

Use `smthrs runs` to read persistent run state. Start with the run's status, then narrow the investigation to its events or an individual output.

You need the project that owns the run's `.flows/` directory, or a connection to its [control plane](/docs/guides/control-plane/). Pass `--root <project>` for another local project, or `--remote <url>` and its credential for a remote host.

## Find the run

List failed runs for a flow:

```bash
smthrs runs list --flow deploy/status --status failed
```

Replace `deploy/status` with your flow ID. Omit the filters to list the project's runs. The listing includes run IDs, flow IDs, status, waiting reason, and timestamps.

The status filter accepts `accepted`, `running`, `parked`, `waiting-approval`, `cancelled`, `completed`, and `failed`.

## Read the diagnosis

Replace `<run-id>` in these commands with an ID from the listing:

```bash
smthrs runs show <run-id>
```

The result combines current run state with a diagnosis derived from recorded events. Look for the failure cause or waiting reason before reading the full history.

For a script, request JSON explicitly:

```bash
smthrs runs show <run-id> --format json
```

The command reads state without resuming or changing the run. Its exit code reports whether that read succeeded. Check the returned `status` to determine the run's outcome.

## Read or follow events

Read at most 100 events:

```bash
smthrs runs logs <run-id> --limit 100 --format jsonl
```

Continue after a sequence number returned by the previous read:

```bash
smthrs runs logs <run-id> --after 100 --limit 100 --format jsonl
```

Replace `100` after `--after` with the last sequence you received. Sequence numbers can have gaps; do not infer an event count from them.

To follow new events as they commit:

```bash
smthrs runs logs <run-id> --follow
```

Interrupt the log reader when you have enough evidence. Stopping a reader does not cancel the run. Use `--format jsonl` when another program consumes the event stream.

## Read recorded outputs

List the recorded outputs, then request the node you need:

```bash
smthrs runs output <run-id>
smthrs runs output <run-id> result
```

The reserved node `result` names the final assistant output. Other node IDs identify recorded calls. A call that started without settling can appear as `pending`; it has no completed value to retrieve.

## Act on the diagnosis

| Finding | Next step |
| --- | --- |
| `waiting-approval` | [Submit the exact request and resume](/docs/troubleshooting/waiting-approval/). |
| No progress or no executor | [Diagnose a stuck run](/docs/troubleshooting/stuck-run/). |
| A provider refusal | [Check the model seat and credentials](/docs/troubleshooting/doctor-provider/). |
| A completed run needs historical inspection | [Inspect or replay a frame](/docs/concepts/time-travel/). |

For agent access to the same run state, see the [MCP tools reference](/docs/reference/mcp-tools/). For pipeline exit handling, see [Run flows in CI](/docs/guides/ci/).

---

# Configure model seats and providers
https://smithers.sh/docs/guides/model-seats/

A seat is one `provider:model` string that decides which model serves a flow's agent steps. This guide sets the credential that resolves it and changes the seat itself.

## Before you start

- The Smithers CLI: `npm install --global @smthrs/cli@next`.
- A Smithers project. `smthrs init` scaffolds one.
- An API key for the selected provider, or a supported subscription session for an OpenAI seat.

## 1. Read the seat your flow declares

The declared seat lives in the flow source. A markdown flow carries it on the `model:` frontmatter line of `flows/<name>/flow.mdx`; a module flow declares it as the `seat` option of `AgentAction.make`:

**flow.mdx**

```md
---
description: Reviews the uncommitted change in this repository.
capabilities: ["fs:read:**"]
model: anthropic:claude-sonnet-4-5
---
```

**flow.ts**

```ts
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Schema from "effect/Schema"

export const Reviewer = AgentAction.make("review/Reviewer", {
  payload: { change: Schema.String },
  output: Schema.Struct({ verdict: Schema.String }),
  system: ["Review the supplied change."],
  prompt: ({ change }) => change,
  seat: "anthropic:claude-sonnet-4-5"
})
```

The string holds no credential, endpoint, or client. At launch the CLI's seat resolver turns it into a live model, reading the provider half to pick a route and a key. The CLI resolver supports these routes:

| Seat prefix | Credential the resolver reads | Route it builds |
| --- | --- | --- |
| `anthropic:` | `ANTHROPIC_API_KEY` | Anthropic Messages at `https://api.anthropic.com/v1/messages` |
| `openai:` | `OPENAI_API_KEY`, or the ChatGPT session (step 3) | OpenAI Responses at `https://api.openai.com/v1/responses` |
| `openrouter:` | `OPENROUTER_API_KEY` | OpenRouter through its configured model route. |
| `moonshot:` | `MOONSHOT_API_KEY` | OpenAI-compatible Chat Completions at Moonshot. |
| `gemini:` | `GEMINI_API_KEY`, then `GOOGLE_API_KEY` | Gemini's OpenAI-compatible Chat Completions endpoint. |
| `cerebras:` | `CEREBRAS_API_KEY` | OpenAI-compatible Chat Completions at Cerebras. |

OpenRouter seats spell the model as `openrouter:vendor/model`, for example `openrouter:anthropic/claude-sonnet-4.5`. A seat with no `:` reads as a bare model id on the Anthropic route. Any other provider prefix declines the launch: the resolver reports that no route is configured for that provider.

:::note
`smthrs init` writes the scaffold's `model:` line from the first provider credential your environment sets, in the order `smthrs doctor` reports them: `ANTHROPIC_API_KEY` gives `anthropic:claude-sonnet-4-5`, `OPENAI_API_KEY` gives `openai:gpt-5.6-sol`, and `OPENROUTER_API_KEY` gives `openrouter:anthropic/claude-sonnet-4.5`. Moonshot, Gemini, and Cerebras follow. See the [init reference](/docs/reference/cli/init/) for the full order.
:::

[Agent runtime](/docs/concepts/agent-runtime/) explains the two halves of a seat and what `SeatResolver` does with them.

## 2. Set the provider key

Export the variable for the seat's provider before launching:

```bash
export ANTHROPIC_API_KEY="<your key>"
```

An exported-but-empty variable reads exactly like an unset one, so `export ANTHROPIC_API_KEY=""` configures nothing. With no credential for the seat's provider, the launch is declined: `Set <variable> to run the <seat> seat`.

## 3. Choose how openai seats authenticate

By default an `openai:` seat resolves against `OPENAI_API_KEY`. To run it on a ChatGPT subscription instead, select the codex CLI's session and sign in:

```bash
export SMITHERS_OPENAI_AUTH=chatgpt
codex login
```

The seat string does not change: the declared seat remains the same while authentication uses the configured Codex session store. The same seat then routes to the ChatGPT backend at `https://chatgpt.com/backend-api/codex/responses`. With the mode selected but no session on disk, the launch is declined with a message that names `codex login` and the credential file it looked for. For an `openai:` seat, a value other than `api-key` or `chatgpt` is refused: `SMITHERS_OPENAI_AUTH must be "api-key" or "chatgpt" to run the <seat> seat`.

## 4. Change the seat

To change the seat for every later run, edit the flow source: the `model:` line in `flow.mdx`, or the `seat:` option in the module. Discovery reads the source at launch, so the next `smthrs flow start` uses the edited seat.

To change how a seat authenticates for one invocation, prefix the command with the environment variable:

```bash
SMITHERS_OPENAI_AUTH=chatgpt smthrs flow start reviewer
```

The prefix scopes the change to this launch; the flow source keeps its `model:` line.

## Verify it worked

Run `smthrs doctor` and read the `providers` line:

```text
ok   providers: ANTHROPIC_API_KEY
```

The line lists every key the resolver can see, notes any that are exported but empty, and appends `; openai seats use the ChatGPT session` when `SMITHERS_OPENAI_AUTH=chatgpt`. Then launch a flow and confirm which seat served it: `smthrs runs show <run-id>` returns the run and its diagnosis.

## See also

- [Agent runtime](/docs/concepts/agent-runtime/): seats and seat resolution, as a concept.
- [`smthrs doctor`](/docs/reference/cli/doctor/): every check the report runs, including `providers`.
- [Environment variables](/docs/reference/environment-variables/): `SMITHERS_OPENAI_AUTH` and the rest of the `SMITHERS_*` set.
- [Doctor reports a provider problem](/docs/troubleshooting/doctor-provider/): fixes when the `providers` check warns or fails.

---

# Set quotas, budgets, and corrections for agents
https://smithers.sh/docs/guides/agent-policies/

Declare a token and latency budget on a markdown flow. In a module composition, use `AgentAction` to park through quota windows and bound the re-prompts a structured-output step may spend.

## Before you start

- The Smithers CLI and a project with an agent flow. [Build your first agent flow](/docs/tutorials/first-agent-flow/) produces one.
- A provider credential for the flow's seat. [Configure model seats](/docs/guides/model-seats/) covers the keys.
- For durable quota waits and schema corrections, an application host composing `AgentAction` on the durable engine. [Schema-enforced agent outputs](/docs/tutorials/agent-outputs/) shows that composition.

## 1. Declare a budget on the flow

Add a `budget` block to the frontmatter of `flows/<name>/flow.mdx`:

```md
---
description: Reviews the uncommitted change in this repository.
capabilities: ["fs:read:**"]
model: anthropic:claude-sonnet-4-5
budget:
  tokens: 120000
  milliseconds: 900000
---
```

`tokens` sets the token budget; `milliseconds` sets the elapsed-time budget. Either key may stand alone. The CLI turns the declaration into the run's approved envelope and enforces it at the model boundary, so every model call counts toward it, whichever step made the call. Enforcement projects the cost of the coming call from the largest call the run has made, and the first call is never refused. A call can exceed its estimate, and a call already in progress can outlast the latency budget. These checks do not guarantee an exact spending ceiling. A flow that declares no budget runs unbounded.

A malformed budget is ignored rather than tightened: discovery records an `invalid_budget` warning and the flow runs unbounded, because a mistyped ceiling read as zero would refuse the run's first call. `smthrs doctor` prints the warning as a `registry <path>` line.

For a module composition, state the policy directly:

```ts
import * as Budget from "@smthrs/agent/Budget"

const budget = Budget.layer({ tokens: { max: 120000, onExceeded: "warn" } })
```

`onExceeded` decides what running out means:

| Setting | What happens at the ceiling |
| --- | --- |
| `fail` | The step fails with `BudgetExceeded`. |
| `warn` | A budget-warning record is written to the journal and the call proceeds. |
| `skip-remaining` | The budget latches; every later model call in the run fails without asking a provider. |

To give up token and latency ceilings in writing, bind `Budget.layerUnbounded()`.

## 2. Park a composed agent action through quota windows

The CLI's markdown executor currently reports a terminal provider refusal as a failed run; supplying its quota classifier does not install a durable retry loop. Inspect the failure with `smthrs runs show <run-id>` and correct the provider or account condition before starting another run.

Durable quota parking is implemented by `AgentAction` in a module composition. Provide `QuotaPolicy.layerDefault()` to that host. It classifies `rate_limited` and `quota_exceeded`, plus refusals carrying HTTP 429 or 529. `provider_internal`, HTTP 503, and HTTP 504 alone do not qualify. The wake time comes from `resetAtEpochMillis`, then `retryAfterMillis`, then a delay parsed from the message, then the default 60 seconds. A wait beyond the default one-hour maximum propagates the original error.

The decision is a recorded step, so reopening the same durable execution waits out the recorded deadline. The retried call is another attempt of the step. One ask takes at most eight quota waits by default; the host can change `maxQuotaParks`. These options customize the classifier:

```ts
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"

const quota = QuotaPolicy.layerDefault({
  defaultWaitMillis: 60000,
  maxWaitMillis: 3600000
})
```

Provide this layer alongside the engine and budget when registering the agent action. To keep capacity refusals as failures, provide `QuotaPolicy.layerUnclassified()` instead. [Example 39](/docs/examples/39-agent-policies/) includes the complete registration and restart path.

## 3. Spend corrections on schema misses

A module's `AgentAction` declares its answer shape with `output`, and the runtime enforces that schema against the run's final answer. A decode miss spends one correction slot on a re-prompt that repeats the task verbatim with the validation issues appended:

```ts
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Schema from "effect/Schema"

export const Reviewer = AgentAction.make("review/Reviewer", {
  payload: { diff: Schema.String },
  output: Schema.Struct({
    approved: Schema.Boolean,
    issues: Schema.Array(Schema.String)
  }),
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You review diffs and report whether they are approvable."],
  prompt: ({ diff }) => `Review this diff:\n${diff}`,
  corrections: 1
})
```

- `corrections: 1` gives the step one re-prompt. `corrections: 0` makes the first miss terminal. The step's declaration always beats the composition default, including at zero.
- Undeclared, the step falls back to the host's `defaultCorrections`; with neither declared, the budget is one.
- `repair` adds one bounded ask after the correction budget is spent: your own prompt, written from the failure, asked once, on its own seat and teaching when the declaration says so, and decoded by the same schema. A repair is not another correction rung.

Every rejection is journaled with the action, the attempt, the budget, and digests of the schema and the issues, so you can read which correction was spent without the answers entering run state.

## Verify it worked

Run the markdown flow to check its declared budget and read its trail back:

```bash
smthrs flow start reviewer
smthrs runs logs <run-id>
```

- A budget in `warn` mode leaves budget-warning records in the journal and the run keeps going; in `fail` mode the step that would cross the ceiling fails.
- In an `AgentAction` composition, a quota park writes `flows.agent.quota-parked.v1` and the engine records a wait with reason `quota`. Reopening the same execution reuses its recorded deadline.
- A composed action's spent correction produces a rejection record and a re-prompt after the schema miss; the settled answer still decodes against its declared schema.

[Example 39](/docs/examples/39-agent-policies/) drives one step through all three policies on the durable engine across a process kill: the provider refuses with HTTP 429 and `retry-after`, the run parks, a second engine over the same file waits out the recorded deadline, spends a correction, and finishes. The assertions check three provider calls in all, one before the restart, one park decision, one correction.

## See also

- [Agent runtime](/docs/concepts/agent-runtime/): budgets, parks, and the correction ladder in the runtime's own terms.
- [`smthrs runs`](/docs/reference/cli/runs/): inspect and resume a parked run.

---

# Give a flow tools
https://smithers.sh/docs/guides/tools/

A tool in Smithers is an ordinary flow: a declaration carrying a name, schemas, capabilities, and effects, plus a handler the host runs. Giving a flow tools means composing those declarations into the catalog its run sees and granting the authority they declare.

## Before you start

- The Smithers CLI: `npm install --global @smthrs/cli@next`.
- A Smithers project. `smthrs init` scaffolds one.
- For step 1, the library packages:

```bash
pnpm add @smthrs/std@next @smthrs/flow@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112
```

## 1. Call the std tools from a module flow

`@smthrs/std` is the standard tool library. Seventeen declarations ship: `read`, `write`, `edit`, `ls`, `glob`, `grep`, `bash`, `test`, `shell_command`, `apply_patch`, `update_plan`, `fetch`, `http-post`, `explore`, `webfetch`, `websearch`, and `lsp`. Run under the CLI, an agent flow's catalog already carries the filesystem tools (`read`, `write`, `edit`, `apply_patch`, `ls`, `glob`, `grep`), plus `bash`, plus `test` when the host declares a test runner. In your own module flow, call a handler directly inside an action implementation:

```ts
import { Action } from "@smthrs/flow"
import { Read } from "@smthrs/std"
import * as StdError from "@smthrs/std/StdError"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"

export const ReadFile = Action.make("docs/ReadFile", {
  payload: { path: Schema.String },
  success: Schema.String,
  error: StdError.StdError
})

export const readFile = ReadFile.toLayer(({ path }) =>
  Read.run({ path }).pipe(Effect.map((page) => page.content))
)
```

`Read.run` can fail with `StdError.StdError`, so the action declares that error
channel. An action implementation may only call a failing effect when its
declaration states the failures callers must handle.

`Read.run` needs a `FileSystem` service, which the host's layer supplies: the CLI's executor provides the kernel filesystem, and a standalone Node program provides `NodeFileSystem.layer`. Across the library six services are injected, and a tool whose service the host has not bound refuses with `provider_unavailable` rather than a silent success:

| Service | Needed by | Bound by |
| --- | --- | --- |
| `Search` | `grep`, `glob` | `NativeSearch.layer` (ripgrep) or `PortableSearch` (in process) |
| `Container` | `bash`, `test` | `Container.layerCommand` (docker or podman) |
| `TestRunner` | `test` | the repository's own declaration |
| `Checkpoints` | agent-side pinning | the git checkpoint store |
| `WebSearch` | `websearch` | `ExaWebSearch.layer` |
| `LanguageServer` | `lsp` | `NodeLanguageServer.layer` |

Each declaration also carries the authority it needs, which step 4 grants:

| Tools | Declared capabilities |
| --- | --- |
| `read`, `ls`, `glob`, `grep`, `lsp` | `fs:read:/**` |
| `write` | `fs:write:/**` |
| `edit`, `apply_patch` | `fs:read:/**` and `fs:write:/**` |
| `bash`, `test`, `shell_command` | `proc:spawn:*` |
| `fetch`, `webfetch` | `net:get:*` |
| `http-post`, `websearch` | `net:post:*` |
| `update_plan` | none |
| `explore` | the union of the read-only tools' declarations |

To offer the library to a model rather than call it directly, the `Manifest` registries expose `flows` (every declaration by name), `handlers` (the executable subset), `effectsFor` (the per-call envelope narrowing, so one `read` declares the path it was given rather than the registry-time worst case), and `readOnly` (the names a read-only seat may see: `read`, `ls`, `glob`, `grep`, `fetch`, `explore`, `webfetch`, `lsp`). A capped result says so in its own output, with a `truncated` flag or a notice line naming what was shown, never a silent cut.

:::note
The package's root entry point is Node-only. The four search subpaths `@smthrs/std/Grep`, `@smthrs/std/Glob`, `@smthrs/std/Search`, and `@smthrs/std/PortableSearch` are the browser-safe entries.
:::

## 2. Attach an MCP server with --mcp-config

An external MCP server's tools project into a run's flow catalog, one flow per tool. Describe the servers as a JSON array, where each entry is one `McpClient.ConnectOptions`:

```json
[
  {
    "server": "github",
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-github"],
    "env": { "GITHUB_TOKEN": "<token>" }
  }
]
```

The configuration shape is `{server, command, args, cwd?, env?, handshakeTimeoutMs?, requestTimeoutMs?, queueCapacity?, maxFrameBytes?}`: `server` and `command` are strings, `args` is an array of strings, `cwd` is an optional string, and `env` is an optional object of string values that merges into the inherited child environment, so a spawned server still receives `PATH` and `HOME`. The four numeric options are positive integers defaulting to a 10-second handshake, a 120-second call deadline, 64 queued outbound frames, and a 1 MiB inbound frame.

Pass the file when starting the flow:

```bash
smthrs flow start reviewer --mcp-config mcp-servers.json
```

Or set the environment form, which the flag overrides:

```bash
export SMITHERS_MCP_CONFIG="$PWD/mcp-servers.json"
smthrs flow start reviewer
```

Omitting both configures no MCP servers. A named path that is missing, unreadable, not valid JSON, or not an array of entries is a usage error (exit 2) that names the flag, for example `--mcp-config mcp-servers.json: file not found`. A detached run (`flow start -d`) forwards the flag to its child process as an absolute path, so the child reads the same file the parent parsed.

Each named server connects when the executor starts, and one that fails to spawn stops the executor loudly rather than running silently short of the tools it was configured to have.

## 3. Read the projected tool names

Each remote tool becomes a flow named `mcp/<server>/<tool>`: the GitHub server's `create_issue` is the flow `mcp/github/create_issue`. The server name scopes the tool name, so two servers may both offer a `search`. The flow discloses the server's own JSON Schema as its input document, so a caller reading the catalog sees the real parameter shape. The catalog is fetched once at connect time; a server that changes its tools later is not re-polled, so reconnect to refresh.

The projection narrows with three options: `include` (exact tool names to keep), `exclude` (names to drop, applied after `include`), and `namePrefix` (replaces the `mcp/<server>` prefix). The executor passes each config entry to `McpFlows.connected` verbatim, so the same keys work in the `--mcp-config` file:

```json
[
  {
    "server": "github",
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-github"],
    "include": ["create_issue", "get_issue"]
  }
]
```

An `include` that names a tool the server does not offer fails the connection with `tool_not_found`; an empty `namePrefix` fails with `protocol_error`.

A connection requires an installed MCP server and any credentials it needs. Keep the agent execution inside the connection scope; a source returned from a closed scope cannot call the server.

A host composing its own catalog in code gets the same projection from `@smthrs/mcp`: `McpFlows.connected` returns a `FlowBinding.Source` to pass to `FlowBinding.catalog` alongside `StandardFlows.filesystem(...)`, `StandardFlows.shell(...)`, and the host's other sources. The connection lives as long as its scope, and closing the scope tears down the spawned server:

See [the MCP tools example](/docs/examples/22-mcp-tools/) for a complete connection, catalog, and tool call. It keeps the connection open for the call and closes it when the scoped program finishes.

## 4. Grant the capabilities the tools declare

The std tools declare the narrow authorities the table in step 1 lists. A projected MCP tool is opaque code the adapter does not control, so its declaration is the honest one: every action in the capability vocabulary at resource `**`, and an effect envelope reading and writing `**` at tier `irreversible`. The declaration lists one exact `namespace:operation:resource` string per action rather than a wildcard, because the cell boundary parses exactly three colon-separated components and treats anything else as unauthorized.

Authority reaches a run through its approval: the plan's envelope carries the capabilities you are asked to grant, and `smthrs approvals approve` records the decision with a scope. A call that exceeds the grant parks the run at `waiting-approval` on an attended surface. [Declare and grant capabilities](/docs/guides/capabilities/) walks the mechanics, and [Capabilities and the host kernel](/docs/concepts/kernel/) defines the vocabulary.

## Verify it worked

Launch a flow whose agent will call the attached tool, then list the run's recorded calls:

```bash
smthrs flow start reviewer --mcp-config mcp-servers.json
smthrs runs output <run-id>
```

`smthrs runs output` prints one node per call, named `<flowName>#<ordinal>`. A call to the attached server appears under its projected name, for example `mcp/github/create_issue#1`, and `smthrs runs output <run-id> "mcp/github/create_issue#1"` prints what the tool returned. Std tool calls in the same run list as `read#1`, `bash#2`, and the like.

Two failure shapes are distinguishable at this point: a server that failed to spawn failed the launch itself (step 2), and a call the grant refuses parked the run, which [unstick a run waiting on approval](/docs/troubleshooting/waiting-approval/) covers.

## See also

- [Drive Smithers from your agent](/docs/guides/mcp-setup/): the reverse direction, exposing Smithers itself as an MCP server.
- [@smthrs/std API reference](/docs/reference/api/std/): every standard flow with its limits and error codes.
- [@smthrs/mcp API reference](/docs/reference/api/mcp/): `McpClient` and `McpFlows` in full.

---

# Drive Smithers from your agent
https://smithers.sh/docs/guides/mcp-setup/

Connect your agent to `smthrs --mcp` to let it discover targets, plan flows, inspect runs, and submit approvals through the same command handlers as the CLI.

This is the server direction: an external agent calls Smithers. To give a Smithers flow access to external MCP servers, use [Give a flow tools](/docs/guides/tools/).

## Before you start

- Install the [Smithers CLI](/docs/installation/).
- Choose the project the agent should operate. Verify it with `smthrs flow list --root <project>`.
- Use an agent client that supports stdio MCP servers.

## Register the launch command

In your client's MCP settings, add a stdio server with executable `smthrs` and argument `--mcp`. For clients that use a JSON `mcpServers` map, the entry is:

```json
{
  "mcpServers": {
    "smithers": {
      "command": "smthrs",
      "args": ["--mcp"]
    }
  }
}
```

Merge the entry into your existing configuration using your client's documented format. If the client cannot find global npm executables, use the absolute path returned by `command -v smthrs`.

The client starts and owns the server process. A manual `smthrs --mcp` launch waits for protocol input; it does not open an interactive prompt. Keep stdout available for protocol messages.

## Select the project explicitly

An MCP client may launch servers outside the repository you are viewing. Pass the absolute project path as the `root` argument on flow and run tools. Target commands expose their workspace selector in their own schemas.

For remote control, inspect the tool's `remote` and `credential` fields. [Run a control plane](/docs/guides/control-plane/) explains which host executes the work and retains its state.

## Discover and inspect tools

Ask the agent to search for flow tools, inspect `flow_list`, and call it for your project. The server exposes four discovery entry points:

1. `search_tools` finds commands by capability.
2. `get_tool_details` returns a selected command's schema.
3. `call_read_tool` calls a tool explicitly marked read-only.
4. `call_write_tool` calls a writable or unclassified tool.

A command's annotations determine which call wrapper it accepts. For example, `flow_list` currently uses `call_write_tool` because it is unclassified; that classification does not cause the listing to launch a run.

The [MCP reference](/docs/reference/mcp-tools/) gives concrete request objects and command names.

## Plan before launching

Have the agent call `flow_plan` and show you the work and approval payload. Submit that exact payload through `approvals_approve`, then execute it with `flow_execute` when the work is authorized.

`flow_start` combines planning, run-scope approval, and launch. Calling it is authorization to start the flow, so use the separate plan path when review needs to happen first.

Apply tool approval policies in the agent client. The old `--surface` and `--read-only` examples belong to the earlier semantic MCP server and do not configure the current default entry point.

## Verify the connection

The agent's `flow_list` result should contain the same IDs as:

```bash
smthrs flow list --root <project>
```

If the catalog is empty, check the absolute project path and [flow discovery](/docs/guides/flow-discovery/). If the server fails to start, check the executable path and the client's server logs. If a call fails validation, inspect the tool schema again before retrying.

After an authorized launch, confirm its run ID outside the agent:

```bash
smthrs runs show <run-id> --root <project>
```

The result should identify the flow you selected. Continue with [Inspect runs](/docs/guides/inspecting-runs/) for logs and outputs.

---

# Declare and grant capabilities
https://smithers.sh/docs/guides/capabilities/

Declare the host authority a flow needs, grant that authority at approval time, and keep every grant as narrow as the work allows.

## Before you start

- A Smithers project with at least one markdown flow under `flows/`. The [CLI quickstart](/docs/cli-quickstart/) scaffolds one.
- You know what a capability, a grant, and the kernel's closed host surface are. [Capabilities and the host kernel](/docs/concepts/kernel/) defines them; this page only walks the mechanics.

## 1. Declare capabilities in the flow's frontmatter

Add a `capabilities` list to the frontmatter of `flows/<name>/flow.mdx`. Each entry is one `action:resource` pattern. The review flow in this repository reads the working tree and runs commands, and declares exactly that:

```md
---
description: Reviews the uncommitted change in this repository and returns a verdict with the reasons behind it.
capabilities: ["fs:read:**", "proc:spawn:*"]
---
```

The lint flow also edits files, so it adds `fs:write:**`:

```md
---
description: Lints the files you name against this repository's own conventions and fixes what it finds.
capabilities: ["fs:read:**", "fs:write:**", "proc:spawn:*"]
---
```

The action vocabulary is closed: `fs:read`, `fs:write`, `net:get`, `net:post`, `model:call`, `proc:spawn`, and the `jj:*` family. The resource is a glob matched against the whole resource with no path normalization: `*` matches any run of characters, `?` matches exactly one, and there is no escape, so a resource that genuinely contains `*` or `?` cannot be granted exactly. A flow that declares no capabilities gets the whole-authority sentinel `*`, which is what markdown discovery writes and plans persist.

The declared list is what a plan presents for approval: a plan's envelope carries the capabilities, flows, budget, and placement you are being asked to grant.

## 2. Grant the ask with a scope

`smthrs approvals approve <approval>` records the decision, and `--scope` says how far the grant reaches:

```bash
smthrs approvals approve @approval.json --scope run
```

| Scope | The grant covers |
| --- | --- |
| `once` | this ask only (the default) |
| `run` | matching requests in this run |
| `remembered` | later requests that match the remembered policy |

Submit the payload unchanged: the digest inside it is what the grant binds to, so an edited payload is a different ask, not an approved one. `smthrs approvals deny` records the opposite decision.

Behind the verb, the grant store holds four resolutions: `once`, `run`, `remembered`, and `deny`. A remembered grant becomes policy for later runs, and replay activates a run grant only for its own run and current plan digest, so a grant never leaks into a run that did not ask for it.

## 3. Read a typed denial

When a run asks for more than its grant, the answer is typed, not a string to match:

- An attended store suspends the asking fiber on its request. On the CLI the run parks at `waiting-approval`, and the approval payload of the parked run's `control.approval.requested` event is what step 2 approves or denies.
- An unattended store fails immediately with `PermissionRequired`.

The stable codes are `permission_required` and `permission_denied`, plus the `GrantStoreErrorCode` values `duplicate_request`, `request_not_found`, `journal_failed`, `store_closed`, and `invalid_resolution`.

Through Effect's own `FileSystem` and `ChildProcessSpawner` services the same failure arrives as a `PlatformError` whose reason is `PermissionDenied`: `description` carries the one-line `Permission.formatError` rendering, and `cause` carries the structured failure itself. `Permission.fromPlatformError` recovers the structured value (and `HttpClient.fromHttpClientError` does the same for a network refusal), so an attended surface can still reply to the request and an unattended report can still name the capability.

## 4. Keep grants least-privilege

- Declare reads without writes when the flow only reads files. A broad `proc:spawn:*` grant can still launch programs that modify files; filesystem grants do not make arbitrary subprocesses read-only.
- Name the program in a spawn grant. The resource of a `proc:spawn` check is the rendered command line, and a trailing ` *` also matches the bare program: `proc:spawn:git *` covers `git diff` and bare `git`, and nothing else, where `proc:spawn:*` covers every command.
- Use the default `--scope once` for one request. Choose `run` for matching requests during this run, or `remembered` for a policy that should persist.
- Keep writes inside the workspace. An `fs:write` inside the workspace root is `compensable`; one that escapes it is `irreversible`. A workspace write grant does not cover the system temporary directory, which the kernel authorizes under a sentinel outside the workspace root.
- Never build a pattern by concatenating agent-supplied text. Derive it with `Capability.patternFromCapability`, which returns `Option.none()` for a resource the grammar cannot express exactly.
- Treat an `https` grant as host-only: the resource is the lowercased URL host, and a grant for the host never authorizes a cleartext `http` downgrade. GET and HEAD check as `net:get`; every other method checks as `net:post`.

## Verify it worked

- `smthrs approvals approve` prints an `Accepted` receipt. A plan-level decision answers with no `runId`; a decision on an in-run ask answers with the `runId` it restarted. Exit code `0` means the decision was accepted (or the restarted run settled `completed`); exit code `3` means the run parked again at `waiting-approval`.
- A run that exceeds its grant on an unattended composition fails with `permission_required`. The same run on an attended surface parks at `waiting-approval`; [unstick a run waiting on approval](/docs/troubleshooting/waiting-approval/) covers that state.

## See also

- [Capabilities and the host kernel](/docs/concepts/kernel/): the closed vocabulary, grant stores, and canonicalization this page relies on.
- [smthrs approvals approve](/docs/reference/cli/approvals/) and [smthrs approvals deny](/docs/reference/cli/approvals/): the decision verbs and their exit codes.
- [Plan, approve, run](/docs/guides/plan-approve-run/): the full decision flow from plan card to launch.

---

# Run work in a sandbox
https://smithers.sh/docs/guides/sandboxes/

Place an action body, or a whole child flow, on a provisioned machine while the engine and journal stay on the local SQLite composition.

## Before you start

- The library packages: `pnpm add @smthrs/sandbox@next @smthrs/flow@next effect@4.0.0-rc.112`.
- A durable engine composition to provide the sandbox layers into. The examples below compose one over a local SQLite file.
- Node.js 22.19.0 or later.

## 1. Choose a provider

A provider is the value that turns a session key into a machine. Nine ship in `@smthrs/sandbox`:

| Provider | A machine is |
| --- | --- |
| `DirectorySandbox` | one host directory under `root` |
| `ContainerSandbox` | one container from `image`, held on `sleep infinity`; its network is `none` unless explicitly set |
| `KubernetesSandbox` | one Pod from `image`, held on `sleep infinity` |
| `JustBashSandbox` | one directory in a shared virtual filesystem, commands interpreted in-process |
| `MicrosandboxSandbox` | one local microVM from an image or a snapshot |
| `VercelSandbox` | one named persistent Vercel sandbox |
| `DaytonaSandbox` | one named Daytona sandbox |
| `AwsSandbox` | one Fargate task from `RunTask` |
| `CloudflareSandbox` | one Sandbox Durable Object behind a Worker binding |

When a flow or agent names no provider, `Sandbox.selectProvider(registry, name?)` picks one. The default is `"microsandbox"`, and every other provider stays selectable by name (`"directory"`, `"container"`, `"kubernetes"`, `"just-bash"`, `"vercel"`, `"daytona"`, `"aws"`, `"cloudflare"`). A name the registry does not hold fails with `ProviderError.code === "unavailable"` listing what is registered; nothing falls back to a weaker sandbox silently.

Every bundled provider derives the machine's name from the session key the same way: the key's leading name-safe characters plus a 64-bit digest of the whole key. The digest keeps `a/b` and `a-b` on separate machines, and the stable name is what lets a provider reattach a machine a crashed process left behind.

**Place an action on a sandbox host**

The local engine dispatches an action to a host backed by a sandbox session. File and process operations share that session; the engine records the returned result in local durable state.

## 2. Place one action body somewhere else

Keep the durable engine here and give only the action's implementation layer the sandbox host. `Sandbox.layerHost(provider, { session })` acquires one session for the layer scope and derives `ChildProcessSpawner`, `FileSystem`, and `Path` from it. This excerpt from example 40 uses `fs` and `spawner` from `NodeHost.layerContained()` provided with a `ProcessLedger`; `CountBytes` is the declared action. `DirectorySandbox` refuses raw or deadline-only spawners before creating a workspace:

```ts
import { DirectorySandbox, Sandbox } from "@smthrs/sandbox"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"

const writeAndCount = ({ contents }: { readonly contents: string }) =>
  Effect.gen(function*() {
    const fs = yield* FileSystem.FileSystem
    const spawner = yield* ChildProcessSpawner
    yield* fs.writeFileString("example-40/placed.txt", contents)
    const printed = yield* spawner.string(ChildProcess.make("wc", ["-c", "example-40/placed.txt"]))
    const count = Number.parseInt(printed.trim(), 10)
    if (!Number.isSafeInteger(count)) {
      return yield* Effect.die(new Error(`wc printed an invalid byte count: ${printed}`))
    }
    return count
  }).pipe(Effect.orDie)

const placedHost = Sandbox.layerHost(
  DirectorySandbox.make({ fs, spawner, root: options.root }),
  { session: "examples/sandbox-placement" }
)

const stack = Layer.mergeAll(
  CountBytes.toLayer((payload) =>
    writeAndCount(payload).pipe(
      Effect.provide(placedHost),
      Effect.orDie
    )
  ),
  Interpreter.layer(SandboxPlacement)
)
```

The body knows none of the placement: it asks for Effect's ordinary services, writes a relative path, and runs `wc -c` against it. Both services project from the same session, which is why the process sees the file without either operation naming a provider or a remote path. The action's execution scope owns the host layer: completing the action closes that scope, releases the session, and removes the scratch workspace. The engine still plans the flow, dispatches the action, and journals its result through the local composition.

`DirectorySandbox` 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. The same is true of `JustBashSandbox`. For an isolation boundary, choose a provider whose machine boundary denies ambient host access, such as a container, a Pod, or a microVM.

## 3. Run a whole child flow inside the machine

`SandboxedFlow` defaults to `runtime: "node"`. Set it to `"bun"` or an executable path to use another runtime. The runtime and bundle path are each quoted as one shell word; use a wrapper script when the runtime needs flags.

`@smthrs/flows/SandboxedFlow` runs the child flow's own code inside the machine as one durable action of the parent. Declare the action over the child's payload schema, then implement it with a session key derived from the parent execution. This is the heart of example 41; `Greet` and `Greeting` come from the child module (`./sandboxed-child.ts` in the example):

```ts
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as SandboxedFlow from "@smthrs/flows/SandboxedFlow"
import { DirectorySandbox } from "@smthrs/sandbox"
import * as Schema from "effect/Schema"

export const RunGreet = SandboxedFlow.action(Greet)

export const SandboxedGreeting = Flow.make("examples/SandboxedGreeting", {
  payload: { name: Schema.String },
  success: SandboxedFlow.resultSchema(Greeting),
  error: SandboxedFlow.SandboxedFlowError,
  body: (payload) => RunGreet.call(payload)
})

const stack = Layer.mergeAll(
  SandboxedFlow.toLayer(RunGreet, Greet, ({ executionId }) => ({
    provider,
    session: `greet:${executionId}`,
    entry: new URL("./sandboxed-child.ts", import.meta.url),
    collectDiff: true
  })),
  Interpreter.layer(SandboxedGreeting)
)
```

The helper bundles the child's module with a guest runner, writes the bundle and a request into the session workspace, starts `node` on it inside the machine, and validates the result the guest wrote against the child's own success schema. The child's TypeScript never runs in the parent process.

From the parent's point of view the whole sandboxed execution is one action with one journaled attempt: a second run over the same SQLite file answers the recorded result without acquiring a machine at all. With `collectDiff: true`, files only the guest wrote come back as data; applying them anywhere is the caller's decision.

## 4. Read the conformance caveats

The providers share one session contract, but their limits differ:

- Standard input is bounded at 16 MiB and refused above it, counted as the bytes arrive. A command's output is collected whole, in memory.
- Command output is byte-exact through `DirectorySandbox`, `ContainerSandbox`, `KubernetesSandbox`, `MicrosandboxSandbox`, and `JustBashSandbox`. It is not through `VercelSandbox`, `DaytonaSandbox`, or `CloudflareSandbox`, whose vendor APIs report output as a string re-encoded as UTF-8, so a command writing a tarball or a compiled binary to stdout comes back changed. `AwsSandbox` reframes output through a pseudo-terminal, which normalizes line endings and interleaves standard error.
- File transfer is byte-exact on all nine. To get bytes out of a command on a string-output provider, have the command write a file and read that back with `readFile`.
- `JustBashSandbox`, `MicrosandboxSandbox`, `VercelSandbox`, `DaytonaSandbox`, and `CloudflareSandbox` declare no `kill`, and on those providers output arrives after the command finishes rather than streaming.
- `Sandbox.layerHost` does not swap a dead machine mid-action. A dead machine surfaces as a failure, because opening a fresh session behind the body's back would silently discard the writes it already made; re-provisioning belongs to whoever retries the action, which acquires the session key again.
- Real-backend suites in this repository cover host directories and processes, Docker, OrbStack Kubernetes, and Microsandbox. The remaining providers are proven against fakes that keep the vendor API shapes. For a provider you compose yourself, run the conformance suites in the verification step rather than trusting the table.

## Verify it worked

- The placed action answers with what the sandboxed process printed: example 40's flow resolves to the byte count `wc -c` reported for the file the body wrote inside the session.
- Run the sandboxed child flow twice over the same database file. The first run acquires a machine; the second answers the recorded result and the provider is asked for nothing.
- For your own provider composition, run `SandboxConformance.check(provider, ...)` (the machine contract: files and processes see the same tree) and, for a spawn-only provider, `ProviderConformance.check(provider, ...)`. Both return a violations array, and a conforming provider's array is empty.

## See also

- [@smthrs/sandbox API reference](/docs/reference/api/sandbox/): the session contract, health probes, supervision, and per-provider options.
- [Compose child flows](/docs/guides/child-flows/): `.child` boundaries, cancel propagation, and detached runs.
- [Capabilities and the host kernel](/docs/concepts/kernel/): the host service tags a sandbox satisfies.

---

# Wait durably: sleep, poll, deferreds, queues
https://smithers.sh/docs/guides/durable-waits/

Wait inside a flow without holding a process open: the wait is recorded, the run parks, and a fresh process resumes it.

## Before you start

- The flow package: `pnpm add @smthrs/flow@next effect@4.0.0-rc.112`.
- You know the journal is what makes a wait survive a crash. [Durable execution](/docs/concepts/durable-execution/) covers the mechanism; this page covers choosing and composing the primitives.

## 1. Pick the primitive

| You are waiting for | Use |
| --- | --- |
| time to pass | `Sleep.action` in a body, or `DurableClock` inside an effect |
| something outside the run to answer | `WaitFor.action`, resolved through `DurableDeferred` |
| a condition to hold | `Poll.make` |
| a worker to take work | `DurableQueue` |

## 2. Wait for time with Sleep

`Sleep.action` is the declared `system/sleep` step: a wait that is a keyed plan node rather than a second execution mechanism. Call it in a body wherever a round has to wait for time to pass, and provide `Sleep.layer` beside the other implementation layers:

```ts
import { Sleep } from "@smthrs/flow"

Sleep.action.call({ millis: 60_000 })
```

A payload names exactly one deadline: a relative `millis`, or an absolute `until` in epoch milliseconds. Two waits of the same length are two waits: each call is its own node with its own identity, so `sleep` followed by `sleep` waits twice. A deadline that has already passed settles the node instead of parking it, because a run that resumes after its own deadline has to make progress.

`SleepRequestInvalid` is the typed refusal, with three codes: `missing_deadline` (the payload named neither field), `ambiguous_deadline` (it named both, which are the same fact stated two ways), and `invalid_deadline` (a number that is not a length of time, such as a negative `millis`).

Inside an effect rather than a body, `DurableClock.sleep({ name, duration, inMemoryThreshold? })` parks the execution instead of holding a fiber, so the wait outlives the process waiting on it.

## 3. Rendezvous with the outside with WaitFor and DurableDeferred

`WaitFor.action` is the declared `system/wait-for` step. A payload names the wait point by `name`, relative to the running execution, or by an absolute `token`. `WaitFor.deferred(name)` is the resolver's half, and the completion goes through `DurableDeferred`:

```ts
import { DurableDeferred, WaitFor } from "@smthrs/flow"

const gate = WaitFor.deferred("approval")
const token = DurableDeferred.tokenFromExecutionId(gate, { flow, executionId })
yield* DurableDeferred.succeed(gate, { token, value: { approved: true } })
```

A token encodes the flow name, the execution id, and the deferred name, so another process can complete the correct durable address. Completing a deferred is first-writer-wins: the first recorded exit is the one every later read replays. `TokenInvalid` is 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.

`WaitForRequestInvalid` carries four codes: `missing_target` and `ambiguous_target` for a payload naming neither or both targets, `malformed_token` for a token that does not parse, and `foreign_execution` for a token addressed to another flow or another execution. A deferred result is recorded against the flow and execution that own it, so awaiting a foreign token would park forever while the value it names was recorded elsewhere.

## 4. Poll until a condition holds with Poll.make

`Poll.make(tag, options)` 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 own output or sleep this attempt's delay and hand off to the next round with the attempt counter raised. The declaration and its composition, from example 34:

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

export const Status = Action.make("examples/Status", {
  payload: { id: Schema.String, attempt: Schema.Number },
  success: Poll.CheckResult(Schema.String)
})

export const Deployment = Poll.make("examples/Deployment", {
  input: { id: Schema.String },
  result: Schema.String,
  intervalMs: 120,
  backoff: "fixed",
  maxAttempts: 5,
  onTimeout: "fail",
  check: ({ attempt, id }) => Status.call({ attempt, id })
})
```

```ts
Layer.mergeAll(status, Poll.layer, Sleep.layer, Interpreter.layer(Deployment))
```

`Sleep.layer` is not optional in that composition: the wait between attempts is an ordinary `system/sleep` node, so a composition without it has a plan node no implementation answers.

The schedule options are `intervalMs` and `backoff`: `fixed`, `linear` (interval multiplied by attempt), or `exponential` (interval multiplied by 2 raised to attempt minus one). `maxAttempts` bounds the attempts and is also the flow's `maxRounds`, so a lineage that opened another round is refused by the engine. `onTimeout` is `fail`, which fails `PollExhausted` (stable code `poll_exhausted`) at the bound, or `return-last`, which answers with the last check output. The `check` fragment returns `{ satisfied, output }` and may not fail; state what a failure means with `Node.catch` inside the fragment. `Poll.make` refuses a schedule no clock can keep with a `RangeError` naming the option that is wrong.

**What survives a restart:** each attempt is a durable round with its own keyed plan nodes, and the wait between attempts is a durable timer. Example 34 runs the first attempt, parks on the timer, and drops the engine while the timer is still pending; a fresh engine attached after the timer is due drives the poll to a satisfied check. The recorded dispatches show one check per attempt across both engines: the round that wakes replays the attempt it already made instead of making it again, and the round that is sleeping holds no process.

## 5. Hand work to a worker with DurableQueue

`DurableQueue` is work handoff over Effect's `PersistedQueue`. `DurableQueue.process` offers a payload, attaches a token, and suspends until a worker records the handler's exit against it. Its `retrySchedule` option bounds how a failing offer is retried; the default retries with exponential backoff, capped at one minute, and never gives up before the call dies. `DurableQueue.worker` and `DurableQueue.makeWorker` take a `concurrency` that must be a positive safe integer.

A queue needs an Effect `PersistedQueue` service backed by durable storage. The examples above do not supply one. Use the [DurableQueue API reference](/docs/reference/api/flow/) when adding a queue to your host composition.

## Verify it worked

- Kill a poll mid-wait and attach a fresh engine over the same SQLite file. The lineage finishes, and the recorded check dispatches number one per attempt across both engines. That is example 34's two-phase shape: phase one parks on the timer with the engine dropped underneath it, phase two attaches after the timer is due.
- Feed `Sleep.action` a payload naming both `millis` and `until` and the step refuses with `SleepRequestInvalid` code `ambiguous_deadline` before any timer is armed.
- Let a poll reach `maxAttempts` unsatisfied: with `onTimeout: "fail"` the lineage fails `PollExhausted`; with `return-last` it answers the last check output.
- Complete a wait point from another process with `DurableDeferred.succeed` and the parked run resumes; completing it a second time changes nothing, because the first recorded exit is the one every later read replays.

## See also

- [Steer a run](/docs/guides/steering/): resume, interrupt, and answer parked runs from the CLI.
- [Durable execution](/docs/concepts/durable-execution/): the journal mechanics that make a wait survivable.
- [@smthrs/flow API reference](/docs/reference/api/flow/): the full option tables for `Sleep`, `WaitFor`, `Poll`, `DurableDeferred`, `DurableClock`, and `DurableQueue`.

---

# Compose child flows
https://smithers.sh/docs/guides/child-flows/

Run one flow from inside another as a separate durable run, join the results, and control what happens to children when the parent exits.

## Before you start

- The packages: `pnpm add @smthrs/flow@next @smthrs/plan@next effect@4.0.0-rc.112`. The detached-children half also uses `@smthrs/agent@next`.
- The `durable-layer.ts` composition from [the first-flow tutorial](/docs/tutorials/first-flow/). The examples below use a local SQLite file.

## 1. Choose the boundary

| Form | What it is |
| --- | --- |
| `flow.call(payload)` | splices the callee's body into the caller's plan: one plan, one run, every inner step visible and individually keyed |
| `flow.child(payload)` | one node in the caller's plan and a real child execution when that node is driven: the child's own run row, claim, journal, retry policy, and placement |
| `flow.to(payload)` | a serializable invocation for the next trampoline round: the handoff ends this execution and names the next one |
| `Children.spawn(...)` | a detached child: started now, collected later, possibly by another process |

With `.child`, the parent suspends while the child is unsettled and resumes when it settles, and the engine records the lineage edge in `flows_run_parents` so the relationship survives the process that created it. The interpreter derives the child's execution id from the parent's id and the node's address, so a re-driven parent lands on the child it already started. Children are ordinary flows; nothing marks a flow as a child.

**A parent joins two attached child runs**

The parent starts Compile and Notarize as separate child runs, waits for both results, then passes them to Report. Each child has its own recorded execution state.

## 2. Spawn two children and join their results

`Node.all` settles two children concurrently, and a fan-in step reads each result off the joined value as a payload field. Create `children.ts` with the actions and the three flow declarations:

```ts title="children.ts"
import { Action, Flow } from "@smthrs/flow"
import { Node } from "@smthrs/plan"
import type * as Planned from "@smthrs/plan/Planned"
import * as Schema from "effect/Schema"

export const Bundle = Action.make("examples/Bundle", {
  payload: { target: Schema.String },
  success: Schema.String
})

export const Sign = Action.make("examples/Sign", {
  payload: { target: Schema.String },
  success: Schema.String
})

export const Report = Action.make("examples/Report", {
  payload: { bundle: Schema.String, signature: Schema.String },
  success: Schema.String
})

export const Compile = Flow.make("examples/Compile", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload: { readonly target: string }) => Bundle.call(payload)
})

export const Notarize = Flow.make("examples/Notarize", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload: { readonly target: string }) => Sign.call(payload)
})

export const Release = Flow.make("examples/Release", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: ({ target }: { readonly target: string }) =>
    Node.all({
      bundle: Compile.child({ target }),
      signature: Notarize.child({ target })
    }).pipe(
      Node.bindPlanned((results: Planned.Planned<{ readonly bundle: string; readonly signature: string }>) =>
        Report.call({ bundle: results.bundle, signature: results.signature })
      )
    )
})
```

Register each flow with its own `Interpreter.layer`. Re-executing the parent under the same execution id observes each child's persisted result instead of starting a second child under the same id, so the children's bodies run once across both executions.

Create `run-children.ts` to execute the parent twice:

```ts title="run-children.ts"
import { Action, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { Bundle, Compile, Notarize, Release, Report, Sign } from "./children.ts"
import { durableEngine } from "./durable-layer.ts"

const dispatches = { bundle: 0, sign: 0, report: 0 }
const stack = Layer.mergeAll(
  Bundle.toLayer(({ target }) => Effect.sync(() => {
    dispatches.bundle += 1
    return `dist/${target}.js`
  })),
  Sign.toLayer(({ target }) => Effect.sync(() => {
    dispatches.sign += 1
    return `${target}.sig`
  })),
  Report.toLayer(({ bundle, signature }) => Effect.sync(() => {
    dispatches.report += 1
    return `${bundle} + ${signature}`
  })),
  Interpreter.layer(Release),
  Interpreter.layer(Compile),
  Interpreter.layer(Notarize)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(durableEngine("children.sqlite", "children-guide"))
)

const main = Effect.gen(function*() {
  const first = yield* Release.execute({ target: "app" }, { executionId: "release-1" })
  const second = yield* Release.execute({ target: "app" }, { executionId: "release-1" })
  return { first, second, dispatches }
}).pipe(Effect.provide(stack), Effect.scoped, Effect.orDie)

Effect.runPromise(main).then((summary) => console.log(JSON.stringify(summary)))
```

```bash
node run-children.ts
```

Both results are `dist/app.js + app.sig`, and each dispatch counter is 1.

## 3. Cancel a parent and watch the cascade reach its children

Interrupting a parent records cancellation requests for attached descendants in the same transaction as the parent's terminal transition. Each child's driver must still process its request; a child may remain active until that happens. Example 19 defines a parent waiting on this child:

```ts
import { Action, Flow, Interpreter, WaitFor } from "@smthrs/flow"

export const Watch = Flow.make("examples/Watch", {
  payload: { target: Schema.String },
  success: Schema.Json,
  error: WaitFor.WaitForRequestInvalid,
  body: () => WaitFor.action.call({ name: "clearance" })
})

export const Deploy = Flow.make("examples/Deploy", {
  payload: { target: Schema.String },
  success: Schema.Json,
  error: WaitFor.WaitForRequestInvalid,
  body: (payload: { readonly target: string }) => Watch.child(payload)
})
```

```ts
yield* Deploy.interrupt(deployRunId)
```

Two boundaries shape what you observe:

- The parent writes a request, not the child's terminal row. Ownership fencing forbids one run's driver writing state for a run another driver owns, so the child's own driver settles the request at its next boundary.
- The parent journals what it decided: the `flows.engine.interrupted` record names every run the cascade reached in its `cascadedTo` payload, written in the same transaction as the parent's terminal row; recovery can find these requests after a crash.

An attached child, one the parent is waiting on, records `onParentExit: "cancel"`. Cancellation also reaches the operating-system processes the run was holding: the Node host gives every spawned process its own process group, records it in the durable `ProcessLedger`, and signals then kills the group when the action's scope closes, which cancelling the run does.

## 4. Detach a child and collect it after a restart

A detached child is started now and collected later, possibly in another process. Spawn it through `ChildFlows.Children` inside an action implementation. `EngineChildren.layer({ flows: [...] })` names the flows a child may run, and a `spawn` of anything else is `ChildError { code: "not_found" }`. From example 36:

```ts
import { ChildFlows, EngineChildren } from "@smthrs/agent"
import * as Effect from "effect/Effect"

const startDigest = ({ document }: { readonly document: string }) =>
  Effect.gen(function*() {
    const children = yield* ChildFlows.Children
    const spawned = yield* children.spawn({
      flow: Digest._tag,
      input: { document },
      label: "digest"
    })
    return spawned.child
  }).pipe(Effect.orDie)
```

`spawn` returns a child handle without waiting for its result. The engine records `onParentExit: "detach"`, so the parent may complete while the child keeps going. Keep the handle to collect the result later.

To collect, any engine over the same database awaits by child id. Example 36 proves the shape with two engines over one SQLite file, which is what a process restart looks like from the database's point of view: phase one spawns the child and lets the parent complete; phase two is a fresh engine that never spawned anything and answers out of durable state:

```ts
const children = yield* ChildFlows.Children
const collected = yield* children.await({ child: phaseOne.child })
return collected.output
```

## Verify it worked

- Read the lineage back: `runChildren(parentExecutionId)` returns one edge per spawned child, and the child's own state document records the parent. Re-drive the parent under the same execution id and the report matches the first run while each child body's dispatch count stays at one.
- After `interrupt`, durable state tells the story: the parent settles `cancelled`, the child's row carries a cancel request, and the parent's `flows.engine.interrupted` journal record lists the child in `cascadedTo`.
- After a detached spawn, the parent's row is terminal while the child's row shows no cancel request, and a fresh engine's `await` returns the child's output.

## See also

- [@smthrs/flow API reference](/docs/reference/api/flow/): `call`, `child`, `to`, execution identity, and the `Interpreter`.
- [Wait durably](/docs/guides/durable-waits/): the primitives a parked child (or the parent waiting on it) suspends on.
- [Run work in a sandbox](/docs/guides/sandboxes/): place a child flow's own code inside a provisioned machine.

---

# Undo work with compensation
https://smithers.sh/docs/guides/compensation/

Undo work a run already did when the run later fails: inside one execution with `withRollback`, across retries with a compensable action, or across history with rewind-time handlers.

## Before you start

- The packages: `pnpm add @smthrs/flow@next effect@4.0.0-rc.112`. Rewind-time handlers come from `@smthrs/time-travel@next`.
- A `Jj` implementation in the composition for compensable actions. The engine calls `snapshot` before each attempt and `restore` before a retry; if neither does anything, nothing is compensated.
- The tier vocabulary in one clause: reads are `sealed`, undoable writes are `compensable`, and the rest are `irreversible`. [Time travel](/docs/concepts/time-travel/) defines compensation as a protocol over the journal a run wrote.

## 1. Pick the mechanism

| When the undo runs | Mechanism |
| --- | --- |
| the enclosing flow exits unsuccessfully | `flow.withRollback(effect, rollback)` |
| a compensable action is retried | `tier: "compensable"` on the action; the engine restores the workspace pre-image |
| a rewind crosses an irreversible effect | a `CompensationHandlers` entry for that effect kind |

Match the mechanism to the tier. A sealed step changes nothing, so there is nothing to undo. A compensable step's undo is the workspace pre-image the engine already took. An irreversible step (a `net:post`, a `proc:spawn`, an `fs:write` that escapes the workspace) needs a handler, or it cannot be undone at all.

## 2. Declare undo inside one execution with withRollback

Wrap an effect that runs directly inside the flow execution. If the effect fails, no rollback registers. If it succeeds, its value is captured in a flow-scope finalizer that does nothing when the flow succeeds and calls `rollback(value, cause)` when the flow exits unsuccessfully:

```ts
yield* flow.withRollback(
  registerHostname("greeting-ada-1.internal"),
  (hostname, cause) => removeHostname(hostname)
)
```

The shape above is illustrative; `registerHostname` and `removeHostname` are your own effects. Two limits are contractual: `withRollback` applies only to effects run directly inside the flow execution and does not attach rollback behavior to nested actions, and the rollback's typed error channel is `never`, so handle expected rollback failures inside the callback.

## 3. Compensate a workspace-changing action across retries

Declare `tier: "compensable"` on an action whose body changes the workspace. The engine takes a `Jj` pre-image before the attempt and, on a retry, restores that pre-image before the next one. The undo is the engine's, not the body's: an action that fails halfway through leaves nothing behind for attempt two to trip over.

Example 21's migration step is the saga shape: pre-image, attempt writes and fails, restore, attempt writes and succeeds, post-image. The body appends to a real file before it fails, so the pre-image is the only thing standing between attempt two and a doubled migration:

```ts
import { Action } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
import { readFileSync, writeFileSync } from "node:fs"

const applyMigration = Action.make({
  name: "examples/ApplyMigrationStep",
  success: Schema.String,
  error: Interrupted,
  tier: "compensable",
  execute: Effect.gen(function*() {
    const attempt = yield* Action.CurrentAttempt
    attempts.push(attempt)
    yield* Effect.sync(() => {
      writeFileSync(ledger, `${readFileSync(ledger, "utf8")}ALTER TABLE runs ADD COLUMN lane;\n`)
    })
    // The write is already on disk. Failing here is what a half-applied
    // migration looks like, and it is what the pre-image undoes.
    if (attempt === 1) return yield* Effect.fail(new Interrupted({ attempt }))
    return "applied"
  })
})
```

`Interrupted` is the example's declared tagged error, `ledger` is the workspace file, and the dispatch wraps the action with `Action.retry(applyMigration, { times: 1 })`. The retry ladder can still run out, so the failure stays declared rather than swallowed.

The engine takes two pre-images per attempt and one post-image after it. The pair is not a duplicate: one pre-image is the rollback boundary around the whole compensable dispatch, the other is the attempt row's own, which is what an adopted attempt restores after a crash. The post-image is what the boundary diffs the pre-image against.

## 4. Handle irreversible effects at rewind time

A `TimeTravel.rewind` that crosses an irreversible effect resolves a compensation handler for it. The adapter that performed the effect owns its compensation, so the composition that wires that adapter contributes the handler. Verbatim from the `CompensationHandlers` module:

```ts
import { CompensationHandlers } from "@smthrs/time-travel"

const refund = CompensationHandlers.layer([{
  kind: "billing/charge",
  tier: "irreversible",
  residue: (effect) => `Charge ${effect.id} was refunded, not un-charged.`,
  revert: (effect) => Effect.succeed({ refunded: effect.id }),
  rollback: () => Effect.void
}])
```

The contract that keeps this safe:

- 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 the recorded evidence: 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. A handler with `requiresIdempotencyKey` never reverts an effect recorded without a key.
- `rollback` is required even when the answer is "nothing to undo": a rewind that fails after compensating replays rollbacks in reverse from the receipts `revert` returned, and a silent default would make a forgotten handler indistinguishable from a deliberate no-op.
- Some work cannot be undone. The rewind then fails with code `irreversible` (no handler, or a sealed result whose cache entry is gone), or with `compensation_failed` when a handler or the workspace restore itself fails, and stops rather than leaving the world half reverted.

The library exposes rewind through `TimeTravel`; the CLI exposes it as `smthrs runs rewind <run-id> --at <sequence>`. Use `--preview` to inspect the affected suffix before applying a rewind with `--yes`. See [Time travel](/docs/concepts/time-travel/) for host and run eligibility requirements.

## Verify it worked

- Run example 21's `compensated` scenario: the body sees attempts 1 and 2, the restores log names attempt one's pre-image, and the workspace file on disk holds the base line plus exactly one `ALTER TABLE runs ADD COLUMN lane;` line. The file is the evidence: without the restore, attempt two would have doubled the migration.
- Fail the enclosing flow after a `withRollback` effect succeeds and the rollback runs once with the captured value. Let the flow succeed and the rollback never runs.

## See also

- [Time travel](/docs/concepts/time-travel/): replay, fork, rewind, compensation, and recovery as journal protocols.
- [@smthrs/flow API reference](/docs/reference/api/flow/): `withRollback`, action tiers, and `Action.retry`.
- [Write a retry policy](/docs/tutorials/retry-policy/): the ladder a compensable action climbs.

---

# Share results with artifacts and the step cache
https://smithers.sh/docs/guides/artifacts-cache/

A step's large outputs belong in the content-addressed artifact store, and a sealed step's result belongs in the step cache. Keep both on the local disk, or compose the HTTP tiers so a second engine reuses them without re-executing anything.

## Before you start

- The library packages:

```bash
pnpm add @smthrs/artifacts@next @smthrs/step-cache@next @smthrs/engine-store@next @smthrs/flow@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112
```

- You know what a content address buys: [Content addressing](/docs/concepts/content-addressing/) covers key derivation, and this page only composes stores.
- For the shared tiers, an HTTP cache endpoint. The step-cache tier requires HTTPS unless the endpoint is loopback, so plain HTTP on localhost works for a trial; the artifact tier refuses a non-HTTPS endpoint everywhere.

## 1. Put and get bytes in the local artifact store

`ArtifactStore` provides `put`, `get`, `has`, and `findMissing`. `put` measures the bytes and returns their address, a SHA-256 digest spelled as 64 lowercase hexadecimal characters. `get` verifies that the stored bytes still hash to the requested address before returning them. The filesystem store publishes each blob at `<directory>/<first two hex>/<digest>`, and the directory defaults to `.flows/objects`, which is also where the engine publishes every step output over the inline bound:

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as ArtifactStore from "@smthrs/artifacts/ArtifactStore"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"

const program = Effect.gen(function*() {
  const store = yield* ArtifactStore.ArtifactStore
  const digest = yield* store.put(new TextEncoder().encode("dist/server.js"))
  return yield* store.get(digest)
})

const store = ArtifactStore.layerFileSystem().pipe(Layer.provide(NodeFileSystem.layer))

await Effect.runPromise(
  program.pipe(Effect.provide(store), Effect.provide(NodeCrypto.layer))
)
```

A valid address with no bytes behind it fails with `ArtifactMissing`; bytes that no longer hash to their address fail with `ArtifactCorruption`; a malformed address is an `ArtifactStoreError` with code `invalid_digest`. `ArtifactStore.makeMemory()` builds an in-memory store for tests.

## 2. Add the remote artifact tier

`RemoteArtifacts` speaks the shared tier's protocol over Effect's `HttpClient`: `GET`, `PUT`, and `HEAD` at `/cas/{digest}`, and `POST` at `/cas/findMissing`, with every download bounded and digest-verified before it is returned. The endpoint and its headers are construction options, never step inputs: construction refuses a non-HTTPS endpoint, and one carrying credentials, a query, or a fragment, with `invalid_configuration`.

`CombinedArtifacts` composes the two tiers. It reads the local tier first and falls back to the remote tier only for `ArtifactMissing` or `ArtifactCorruption`; a local host refusal does not silently fall through. `put` publishes locally first and returns the local digest, and the remote upload is opportunistic, so a remote refusal or timeout does not fail the operation that produced the bytes:

```ts
import * as CombinedArtifacts from "@smthrs/artifacts/CombinedArtifacts"
import * as RemoteArtifacts from "@smthrs/artifacts/RemoteArtifacts"
import * as FileSystem from "effect/FileSystem"

const combined = Effect.gen(function*() {
  const fileSystem = yield* FileSystem.FileSystem
  const local = ArtifactStore.makeFileSystem(fileSystem)
  const remote = yield* RemoteArtifacts.make({ endpoint: "https://cas.example.com" })
  return yield* CombinedArtifacts.make({ local, remote })
})
```

The composition needs a `FileSystem` and an `HttpClient` in scope. The remote tier declares its materialization policy, and `CombinedArtifacts.Options.downloadPolicy` overrides it for one composition; with neither, the policy is `all`:

| Policy | Read-through behavior |
| --- | --- |
| `all` | Write a remotely fetched blob into the local tier. |
| `toplevel` | Write a remotely fetched blob into the local tier when first read. |
| `minimal` | Serve a remote hit without growing the local tier. |

## 3. Declare a step the cache can share

A sealed step becomes shareable with two declarations, and a step missing either stays local:

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

export const Compile = Action.make({
  name: "build/Compile",
  success: Schema.String,
  tier: "sealed",
  idempotencyKey: "build/compile/v1",
  metadata: { readSet: [], writeSet: [], boundaryMode: "hard" },
  execute: Effect.sync(() => "dist/server.js")
})
```

- `idempotencyKey` gives the step an identity another machine can derive. Without one the engine falls back to a run-local invocation key.
- `metadata` declares a HARD file boundary. Only a hermetic step is cacheable across runs, and an empty read set with an empty write set is the declaration for a body that touches no files.

Cache admission (the boundary checks every entry passes before a statement or request is issued: the key grammar, size and shape bounds, and a frozen copy of the value) and the `CurrentCacheEnvironment` a composition declares are what make a result addressable beyond its own run; [Content addressing](/docs/concepts/content-addressing/) explains the key derivation both plug into.

## 4. Share step results over the HTTP action cache

The step cache's remote tier is `RemoteCacheStore`: the same service spoken over HTTP, with `GET`, `PUT`, and `DELETE` on `/ac/{keyDigest}` carrying the entry JSON. The endpoint must be HTTPS unless it is loopback and may carry no userinfo, query, or fragment. `CombinedCacheStore` composes it behind the local SQL store: local first, remote second, writing the shared entry back into the local tier so the next lookup is local.

```ts
import { CacheSync } from "@smthrs/engine-store"
import * as CacheStore from "@smthrs/step-cache/CacheStore"
import * as CombinedCacheStore from "@smthrs/step-cache/CombinedCacheStore"
import * as RemoteCacheStore from "@smthrs/step-cache/RemoteCacheStore"

const remote = RemoteCacheStore.make({ endpoint: "https://ac.example.com" })

const sharedCache = Layer.mergeAll(
  Layer.effect(CacheStore.CacheStore)(
    Effect.gen(function*() {
      const local = yield* CacheStore.make
      return CombinedCacheStore.make({
        local,
        remote: yield* remote,
        publication: "deferred"
      })
    })
  ),
  CacheSync.layer(remote)
)
```

Two placement rules make the composition safe:

- Merge `sharedCache` above the engine's storage layer, so the combined store shadows the local-only `CacheStore` the storage layer provides. Merge `Action.layerCacheEnvironment({ layers: [], capabilities: {} })` beneath the engine: this is the third declaration a shareable step needs, and without it the engine scopes every key to its own run, so two engines derive two digests and neither ever finds the other's row.
- Keep `publication: "deferred"`. The shared write stays out of the transaction that makes the local row durable, and `CacheSync` performs it afterwards. A refused publication never fails the run: the result is already durable on this host, so the engine journals an `unpublished` cache-provenance record and carries on. When you also compose the remote artifact tier, the ordering rule is that a cache entry must never be observable in the shared tier while an artifact it references is missing from the shared artifact tier; `@smthrs/engine-store`'s `ArtifactSync` enforces that around `put`.

A conforming shared tier owes three verbs:

| Request | Conforming answer |
| --- | --- |
| `GET /ac/{keyDigest}` | `200` with the entry JSON, or `404` for a miss |
| `PUT /ac/{keyDigest}` | `201` for a first write, another 2xx when nothing disagrees, `409` when something does |
| `DELETE /ac/{keyDigest}` | 2xx when it removed the entry, `404` when it did not |

:::caution
Two extensions ride on `recordedRunId` and `recordedEventSeq` query parameters, fencing a read or a delete to one provenance. Against a tier that ignores query parameters both degrade silently: a fenced eviction becomes an unconditional `DELETE`, and a fenced lookup becomes a head read. Provenance-fenced reads and evictions require a conforming server.
:::

Eviction and sweeps stay local by design: `CombinedCacheStore` evicts and sweeps the local tier only, and `RemoteCacheStore.sweepExpired` validates its argument and answers `0`, because the shared tier owns its own retention. Note the ledger cost of reading a shared tier: a recorded row whose run names no run on this host is never reclaimed by anything, so a host composing a shared tier accepts `flows_step_cache_recorded` growth proportional to the remote entries it has read.

## Verify it worked

- Local artifacts: after a run whose step returns an output over the inline bound, the blob sits at `.flows/objects/<first two hex of the digest>/<digest>`, and `store.get(digest)` returns bytes identical to the ones `put` accepted.
- Shared cache: run the flow on one engine, then run it again on a second engine over a different database file with the same `endpoint`. The second run returns the same result without executing the step body. Instrument the body with a counter or a log line and it fires once across both engines, while the shared tier's log shows one accepted `PUT /ac/{keyDigest}` from the first engine and one `GET /ac/{keyDigest}` answered `200` from the second. With the shared tier refusing writes, both engines execute the body, both still succeed, and both journal an `unpublished` provenance record.

## See also

- [Back up, gc, and retain run state](/docs/guides/retention/): sweep unreferenced artifacts and bound run history.
- [@smthrs/artifacts API reference](/docs/reference/api/artifacts/): the store contract, coordination modes, and chunked uploads.
- [@smthrs/step-cache API reference](/docs/reference/api/step-cache/): entries, provenance, age bounds, and the full server contract.

---

# Operate the control plane
https://smithers.sh/docs/guides/control-plane/

Serve this project's control plane with `smthrs serve` so other terminals can plan, run, steer, and watch runs against the same `.flows/` state.

## Before you start

- You have a Smithers project: `flows/**` sources plus a `.flows/` state directory. [Project layout](/docs/reference/project-layout/) names every file in it.
- You have the `smthrs` CLI, installed with `npm install --global @smthrs/cli@next`.

## 1. Serve the project

From the project root:

```bash
smthrs serve
```

The server binds `127.0.0.1:3000` by default and keeps serving until the process is interrupted. A loopback bind (`127.0.0.1`, `::1`, or `localhost`) needs no bearer. It accepts only loopback Host values and, when a browser supplies `Origin`, loopback HTTP(S) origins; Origin-less CLI requests remain accepted. To change the bind, pass `--host` and `--port`; to suppress the listening banner, pass `--quiet`.

The server mounts these routes:

| Path | Protocol | Serves |
| --- | --- | --- |
| `/rpc` | http | control rpc |
| `/rpc/ws` | ws | control rpc, including watch |
| `/projections` | http | projection snapshots |
| `/projections/ws` | ws | projection subscriptions |
| `/sync` | http | journal sync |
| `/sync/ws` | ws | journal sync stream |
| `/health` | http | workspace identity |

`GET /health` is unauthenticated. The [HTTP API reference](/docs/reference/http-api/) documents what each route answers; the [journal sync stream](/docs/guides/sync-followers/) on `/sync/ws` is what a read-only follower consumes.

## 2. Bind a non-loopback address safely

The bind rule is strict: loopback needs no bearer but rejects foreign Host and browser Origin values, and anything else needs both `--listen` and a bearer token. An unauthenticated control plane on a network address can launch agents with your credentials, so the server refuses both half-measures.

Supply `SMITHERS_API_KEY` through your secret manager or parent process environment, then pass `--listen`:

```bash
smthrs serve --host 0.0.0.0 --port 3000 --listen
```

`SMITHERS_API_KEY` is the preferred credential channel. The compatibility flag `--credential` warns on stderr, even under `--quiet`, because its value is visible in process listings and can be saved in shell history. The warning never prints the value. A non-loopback bind without `--listen` exits 1:

```text
Refusing to bind <host>: pass --listen to serve on a non-loopback address.
```

A non-loopback bind with no bearer token exits 1:

```text
Refusing to bind <host> without a bearer token: set SMITHERS_API_KEY (preferred) or pass --credential.
```

## 3. Point clients at the server

Verbs that act on runs accept the global `--remote` and `--credential` flags. To drive the served project from another terminal, export `SMITHERS_API_KEY` and pass `--remote`:

```bash
smthrs runs list --remote http://192.0.2.10:3000 --credential "$SMITHERS_API_KEY"
```

`--remote` falls back to `SMITHERS_REMOTE`; `--credential` falls back to `SMITHERS_API_KEY`. To make every invocation in a shell act on the remote plane, export both:

```bash
export SMITHERS_REMOTE=http://192.0.2.10:3000
export SMITHERS_API_KEY="<the bearer token>"
```

Reads render the same over `--remote` as they do locally: `smthrs runs show` states this explicitly for its diagnosis card. The [environment variables reference](/docs/reference/environment-variables/) lists both variables.

## 4. Store client credentials

The server takes its bearer token from `--credential` or `SMITHERS_API_KEY` on every start. Programmatic clients built on `@smthrs/control` keep their own credentials in a `CredentialStore`.

The store only ever sees a `SealedRecord`: an id, an opaque name, base64 ciphertext, a per-record nonce, a `version` counter, and a timestamp. A `CredentialCipher` seals the secret before the record reaches the store, so plaintext never crosses the persistence boundary and a stolen store is ciphertext. Writes are compare-and-set on `version`: a writer that read version *n* commits version *n + 1*, and a concurrent writer that read the same *n* is refused with `CredentialConflict` rather than silently overwriting the winner.

Two adapters ship in the package:

- `SqlCredentialStore.layer`: durable persistence over the ambient control database.
- `CredentialStore.layerMemory`: a process-local store, for tests and short-lived workers.

## 5. Probe the server from a script

`GET /health` needs no credential and answers the workspace's identity: a hash of the project root, the gateway id, the protocol version, and the CLI version. A supervisor probes it to learn which workspace a gateway belongs to. To wait for the server in a script, poll the same route:

```bash
#!/usr/bin/env bash
# Wait for this project's control plane to answer its health probe.
base_url="${SMITHERS_REMOTE:-http://127.0.0.1:3000}"
for attempt in $(seq 1 30); do
  if curl --fail --silent --max-time 2 "$base_url/health" > /dev/null; then
    echo "control plane is answering at $base_url"
    exit 0
  fi
  sleep 1
done
echo "no answer from $base_url/health" >&2
exit 1
```

## Verify it worked

- The banner on stderr prints `smthrs serve listening on http://127.0.0.1:3000`, one line per mounted route with its URL and purpose, and an `auth` line reading `no bearer (loopback Host; loopback browser Origin)` or `bearer token`.
- `curl --fail --silent http://127.0.0.1:3000/health` exits 0 and prints the workspace identity document.
- From a second terminal, `smthrs runs list --remote http://127.0.0.1:3000` prints the same run listing a local `smthrs runs list` prints. A loopback server needs no credential.

## See also

- [Run a read-only follower](/docs/guides/sync-followers/): consume the journal stream this server mounts at `/sync` and `/sync/ws`.
- [`smthrs serve`](/docs/reference/cli/serve/): the full flag, output, and exit-code reference.
- [HTTP API](/docs/reference/http-api/): the routes `serve` mounts.
- [Environment variables](/docs/reference/environment-variables/): `SMITHERS_REMOTE` and `SMITHERS_API_KEY`.

---

# Run a read-only follower
https://smithers.sh/docs/guides/sync-followers/

Follow a run's journal from a second process and fold the entries into your own read model.

A follower is a process that replicates journal entries over the read-only sync protocol to project, inspect, or build read models without holding authority over any run. [Sync and read-only followers](/docs/concepts/sync/) covers the concept; this guide runs one.

## Before you start

- You have a source to follow: a control plane serving `/sync` (see [Operate the control plane](/docs/guides/control-plane/)), or an in-process `SyncServer` over the project's journal.
- You have the packages: `pnpm add @smthrs/sync@next @smthrs/journal@next effect@4.0.0-rc.112`.
- You have a run id to follow. `smthrs runs list` lists the project's runs.

## 1. Follow one run's journal

This excerpt demonstrates the client API with an in-memory transport. It needs the source journal and entries from [example 07](/docs/examples/07-sync-follower/) to produce results; it does not connect to `smthrs serve`. For a network client, provide an Effect RPC transport to `SyncClient.layer` and authenticate that transport as described below.

```ts
import type { JournalEvent } from "@smthrs/journal"
import * as TestSocket from "@smthrs/sync/test/TestSocket"
import * as TestSync from "@smthrs/sync/test/TestSync"
import * as Effect from "effect/Effect"
import * as Stream from "effect/Stream"

const runId = "sync-demo-1" as JournalEvent.RunId

export const follow = Effect.gen(function*() {
  const pair = yield* TestSocket.makePair()
  const sync = yield* TestSync.connect(pair)

  // The source process commits entries; the follower only reads them.
  const entries = yield* sync
    .subscribe({ scope: { _tag: "Run", runId }, cursors: [] })
    .pipe(Stream.take(3), Stream.runCollect)

  return Array.from(entries, (committed) => committed.eventType)
}).pipe(Effect.provide(TestSync.layerTest), Effect.scoped)
```

The protocol is read-only: `Sync.Read` and `Sync.Subscribe` deliver journal entries; `Sync.Snapshot` fetches an explicitly public projection for recovery. A scope names either one run or every run in the workspace.

## 2. Read history first, then follow live

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 the cursors it has acknowledged. So the durable history arrives first, and entries committed after the subscription opens arrive as they land.

Cursors are per run. A cursor stores the last sequence the client was delivered, and journal sequences may have holes, so a cursor means "entries after this number", never "expect the next number to be exactly one greater". Pass `cursors: []` to start at the beginning, and persist the cursors a page returns only after you have applied the batch. A request that names one run twice is refused with `invalid_request`. A live follow that loses its transport reconnects under exponential backoff capped at five seconds, resuming from the acknowledged cursors.

## 3. Authenticate the follower

Every sync request runs as a `SyncPrincipal`, and the read path fails closed: the default principal is anonymous, and the server refuses anonymous access to every non-branch run. There are two sanctioned ways to read as the workspace principal:

- Over RPC, `SyncAuth.layer` verifies the workspace capability the connection presents in the `flows-sync-workspace` header and runs the handler as the workspace principal. A header that is present but malformed, forged, expired, or signed by an unknown key is refused outright rather than downgraded to anonymity.
- In process, code that already owns the journal provides `SyncPrincipal` itself, for example with `SyncPrincipal.layerWorkspace("docs-follower")`. That is the sanctioned bypass, and it is never for a transport.

Authorization happens once, when a subscription opens. The signed expiry travels with the identity, and the stream ends with an `unauthorized` error when that moment arrives. An in-process owner presented no credential and has no deadline.

## 4. Build a read model over the entries

By default a cursor names what has been delivered. To make it name what has been applied, pass `apply`: the callback runs to success before the cursor moves, so a consumer whose own application fails re-receives the entry on its next subscription rather than skipping it. Make the callback idempotent, because a redelivery is exactly what a retry is.

```ts
const following = sync.subscribe({
  scope: { _tag: "Run", runId },
  cursors: [],
  apply: (entry) => upsertReadModel(entry) // the cursor moves only after this succeeds
})
```

`upsertReadModel` stands for your projection: any effect that folds one entry into your own tables.

Compaction is the one case where a cursor names history that no longer exists. The client fails closed: without an explicit `onResync` handler, it propagates the server's `compacted` error without advancing the cursor. The handler must restore the missing state and return the actual restored `RunCursor` for that run, with `afterSeq` at or above `resync.checkpointSeq`. Only then does the client advance the cursor and restart the subscription. An invalid receipt fails with `invalid_request`; a failed restore leaves the cursor unchanged.

Use `sync.snapshot(...)`, the client path to `Sync.Snapshot`, to fetch an explicitly public projection. The host must configure `SyncServer.SnapshotSource` for the requested run, lineage, projection and schema version. Fetching a snapshot alone does not apply state or advance a cursor.

```ts
const following = sync.subscribe({
  scope: { _tag: "Run", runId },
  cursors: [],
  apply: (entry) => upsertReadModel(entry),
  onResync: (resync) => Effect.gen(function*() {
    const snapshot = yield* sync.snapshot({
      ...snapshotIdentity,
      protocolVersion: 1,
      runId: resync.runId,
      atLeastSeq: resync.checkpointSeq
    })
    const cursor = { runId: snapshot.runId, afterSeq: snapshot.seq }
    yield* restoreReadModel(snapshot, cursor)
    return cursor
  })
})
```

`snapshotIdentity` supplies your `lineageId`, `projection`, `projectionVersion` and optional branch `capability`. `restoreReadModel` must validate the application's projection schema and transactionally replace its state and durable cursor before succeeding. Return `snapshot.seq`, which can be newer than the reported compaction floor, so replay starts after the state actually restored.

The compaction response identifies a position; it does not contain your application's projection. You can also rebuild from an authorized local source. `Journal.latestCheckpoint(runId)` requires direct journal access, and its raw execution state is not safe to expose as a public projection.

## Verify it worked

- The subscription's first frames are the run's durable history. Example 07 commits `run.started` and `step.recorded` before the subscription opens and `run.completed` after; its summary reads `caughtUp: ["run.started", "step.recorded"]` and `followed: ["run.completed"]`.
- Commit one more entry in the source process. The follower's stream yields it without the caller opening a new stream; the client replenishes its subscription windows.
- With an `apply` callback that fails on purpose, the next subscription from the client's cursors delivers the same entry again.

## See also

- [Operate the control plane](/docs/guides/control-plane/): the server that mounts `/sync` and `/sync/ws`.
- [Sync and read-only followers](/docs/concepts/sync/): scopes, cursors, bounds, and the failure taxonomy.
- [Examples](/docs/examples/): `07-sync-follower.ts` is the runnable form of this guide.

---

# Export traces, logs, and metrics
https://smithers.sh/docs/guides/observability/

Attach the OTLP exporter to a durable run so its spans, logs, and metric series land in your collector.

## Before you start

- You have a program that runs flows on the durable engine (`@smthrs/engine-store`).
- You have an OTLP collector that accepts HTTP at a base URL, for example `http://localhost:4318`.
- You have the package: `pnpm add @smthrs/observability@next`.

## 1. Attach the OTLP layer

The store packages already open spans through Effect's tracer and update metric counters on their hot paths; what they do not do is export. `Otlp.layerFetch` is that exporter, and providing it is the entire wiring. This is `examples/src/10-telemetry-export.ts` reduced to the essential lines:

```ts
import * as Otlp from "@smthrs/observability/Otlp"
import * as Effect from "effect/Effect"

const telemetry = Otlp.layerFetch({
  baseUrl: "http://localhost:4318",
  serviceName: "deploy-status"
})

// `program` is your durable run, unchanged from its non-telemetry form.
const outcome = yield* program.pipe(Effect.provide(telemetry), Effect.scoped)
```

- Nothing in the flow body or the layers changes for telemetry. Deleting the `Effect.provide(telemetry)` line removes telemetry and changes nothing else.
- Signals post below the base URL at `/v1/logs`, `/v1/metrics`, and `/v1/traces`, JSON-serialized.
- The endpoint must be an absolute `http:` or `https:` URL of at most 2,048 characters carrying no userinfo. Anything else fails layer acquisition with `invalid_exporter_endpoint`. Trim the value before handing it to the builder: a value with a trailing newline or a leading space parses cleanly while the untrimmed original is what the exporter would post to.
- Export failure never fails the run. Effect's exporter retries a transient failure three times, then temporarily disables delivery.
- Export flushes when the layer's scope closes, so read what the collector received after `Effect.scoped` completes. `shutdownTimeout` bounds that final flush.
- `Otlp.layer` is the platform-neutral form that takes an Effect `HttpClient`; `layerFetch` binds the host's global `fetch` and involves no OpenTelemetry SDK.

## 2. Forward logs into the journal

`JournalLogger.layerJournalForwarding` turns the run's Effect log records into durable, structured `telemetry.log` entries on that run's journal:

```ts
import type { JournalEvent } from "@smthrs/journal"
import * as JournalLogger from "@smthrs/observability/JournalLogger"

const runId = "deploy-status-1" as JournalEvent.RunId
const journalLogs = JournalLogger.layerJournalForwarding({ runId })
```

Each record is snapshotted, bounded, and redacted synchronously before it joins an asynchronous queue, so caller mutation after the log call cannot change the queued record, and the journal's own redaction rules run before admission. The durable journal allocates `sourceSeq`, so rebuilding or concurrently running logger layers for one run cannot reuse an identity.

- The queue defaults to 256 records and accepts a `capacity` from 1 through 65,536. A full queue drops the incoming record; the callback never blocks.
- A snapshot accepts at most 1 MiB of encoded data, 4,096 container members, and 64 container edges. Unreadable values become `[Unrenderable]`, values past a ceiling become `[Truncated]`, and deep values become `[Deep]`.
- Every loss advances `Metric.droppedLogRecords`: one per queue overflow, per journal delivery failure, and per defect the forwarding worker recovers from.
- The layer replaces the ambient logger set unless `mergeWithExisting` is `true`, and sets a minimum level only when you pass `minimumLogLevel`.

## 3. Read the runtime metrics

The package's `Metric` module exports three cross-package runtime signals and one of its own:

| Instrument | Advances when |
| --- | --- |
| `runThroughput` | a run's terminal transition commits |
| `activeSeats` | a gauge held for the lifetime of a production `Agent.run` stream, released on success, failure, or interruption |
| `quotaParks` | a sealed quota decision is first executed, not when that decision is replayed after a wake or restart |
| `droppedLogRecords` | a log record is lost before durable delivery |

Step-cache lookup and write counters remain owned by `@smthrs/step-cache`; this package does not duplicate those handles. Exported series post to `/v1/metrics` with the rest.

To read a counter in-process, use Effect's `Metric.value`. An outcome-dimensioned counter is read through its tagged attribute view: the engine updates only the tagged series, so the bare handle reads the attribute-less series and always sees zero.

```ts
import { EngineStoreMetrics } from "@smthrs/engine-store"
import * as Metric from "effect/Metric"

const dispatches = yield* Metric.value(EngineStoreMetrics.dispatch.Success)
```

## 4. Read one run three ways

The same run is legible through three independent read paths:

1. The CLI. `smthrs runs show deploy-status-1` prints the diagnosis card: a `Verdict` line, a `Run` line, an `Activity` line, token totals, refusals, and a `Next` pointer at `smthrs runs logs`. See [`smthrs runs show`](/docs/reference/cli/status/).
2. The OTLP trace. Every span the packages open posts to `/v1/traces`: the flow lifecycle, the engine dispatch, run claims and heartbeats, journal writes, down to individual `sql.execute` statements. Filter your collector on `service.name` equal to the `serviceName` you passed.
3. The journal projection. `Journal.entries` reads the run's lifecycle events in-process, no collector involved:

```ts
const page = yield* journal.entries({ runId, limit: 200 })
const eventTypes = page.entries.map((entry) => entry.eventType)
```

## Verify it worked

- After the program's scope closes, the shutdown flush has posted and your collector holds the run's spans. Example 10 wraps `fetch` with a recorder for exactly this check: posts to `/v1/traces` carry the span set and posts to `/v1/metrics` carry the series.
- If nothing arrives, check the endpoint first. An unusable one fails layer acquisition with `invalid_exporter_endpoint`; after acquisition, export failure is absorbed by design, so a dead collector never fails the run and looks exactly like a working one that delivers nothing.
- `smthrs runs show <run-id>`, the trace, and the `Journal.entries` event types tell the same story of the same run.

## See also

- [Inspect runs](/docs/guides/inspecting-runs/): the CLI read path across `ps`, `status`, and `logs`.
- [`smthrs runs show`](/docs/reference/cli/status/): the diagnosis card reference.
- [Examples](/docs/examples/): `10-telemetry-export.ts` records exactly what the collector received.

---

# Back up, gc, and retain run state
https://smithers.sh/docs/guides/retention/

Back up a project's state without corrupting it, and bound how much run history and artifact data the project keeps.

## Before you start

- You have a project with a `.flows/` directory. [Project layout](/docs/reference/project-layout/) names every file in it.
- You have the `smthrs` CLI for the gc pass, and `@smthrs/engine-store@next` plus `@smthrs/artifacts@next` for the programmatic knobs.

## 1. Know what lives in `.flows/`

- `control.db`: the control plane's database. The namespaced facts `smthrs memory` reads and writes live here too.
- `engine.db`: the durable engine's database: the journal, the run store, the step cache, and engine state.
- `logs/`: process logs.
- `objects/`: the content-addressed artifact store. `.flows/objects` is the filesystem store's default directory, and every step output over the inline bound lands here by digest.

Nothing in the durable stores forgets on its own, so every bound on history is one you set yourself.

## 2. Back up the project

To take a consistent copy by hand, stop the processes writing to the project, then copy the entire state directory in the same quiet window. Include any SQLite WAL files; copying only the main database files can omit committed transactions:

```bash
mkdir -p /path/to/backup
cp -R .flows /path/to/backup/flows-backup
```

Keep the backup outside `.flows/`. Test a restore into a separate project before relying on it, with no other process writing to the restored state.

The artifact tier has one cross-process hazard: a sweep deleting a blob the frozen database still references. `@smthrs/artifacts` fences it with `ArtifactBackupLease.withLease`, which holds a heartbeat-backed marker while a filesystem backup freezes its database and copies referenced blobs. Blobs may still be published during the backup; a sweep deletion runs under `ArtifactBackupLease.unlessActive`, which returns `None` while a live lease fences the deletion. A crashed lease becomes reclaimable once its heartbeat goes stale.

Build the store and its sweep with the same directory and coordination mode: no runtime check can detect a mismatch, and a sweep running under `coordination: "process"` skips the backup lease entirely.

## 3. Delete terminal runs with `smthrs gc`

To see what a pass would delete, dry-run it first:

```bash
smthrs gc --older-than 30d --dry-run
```

Then run it for real:

```bash
smthrs gc --older-than 30d
```

The pass deletes terminal runs (completed, failed, or cancelled) that finished before the threshold, together with every row those runs own, sweeping both `.flows/control.db` and `.flows/engine.db` so no half of a deleted run survives. It never touches a live run, never deletes a terminal run that a live run stands above or below in the lineage, and never runs against a remote control plane: it reads and writes the local `.flows/` files directly.

- `--older-than` spells a count and a unit: `s` seconds, `m` minutes, `h` hours, `d` days, `w` weeks, as in `12h`, `90m`, or `2w`. The default is `30d`. A spelling the command cannot read, including a zero window such as `0s`, exits 2.
- The sweep document prints `olderThan` as typed, `dryRun`, one report per database, and `failures`. Each report names the `database` file, the threshold as `olderThanMs`, the `runs` oldest first (under `--dry-run`, the runs it would delete), and the `deleted` row counts per table (empty under `--dry-run`). `--json` prints the same document compactly.
- Every database is probed before any write. A database the pass cannot open is a failure, never a report of zero runs: the report still prints, stderr names each file with its reason, and the command exits 1.

Under the hood the pass is `Retention.collect` from `@smthrs/engine-store`, run over each database file. The programmatic knobs (`RetainOptions`) are `olderThanMs` (age measured from `finished_at_ms`; zero collects every terminal run), `limit` (one pass deletes at most 1000 runs, `Retention.defaultLimit`, and repeated passes converge), and `dryRun`. One `journal.transact` deletes every aged terminal run with its attempts, clock deadlines, deferred completions, journal entries, checkpoints, and time-travel archive rows, and an aged run whose descendant is still live is retained. Nothing schedules any of it.

## 4. Sweep unreferenced artifacts

Run deletion does not touch the artifact tier. `@smthrs/engine-store`'s `ArtifactGc` collects it, and collection never runs automatically: `gc()` is an explicit verb, because deletion is the irreversible direction and a human approving a plan must approve the deletions too.

- The mark walks the durable roots: every attempt row of every existing run and every step cache entry, each carrying boundary evidence, plus attempt checkpoints kept conservatively. The step cache itself is never collected; its rows are the roots of the graph. A root row carrying evidence this build cannot decode aborts the collection rather than contributing nothing.
- The sweep deletes every blob outside the live set and older than the grace bound. `graceMs` defaults to `defaultGraceMs`, two weeks; the grace period protects a blob published, or freshened by a dedupe `put`, after the mark began.
- `pins` holds digests live regardless of reachability, and `dryRun` reports without deleting. The opt-in `ArtifactGcPolicy` layer (`graceMs`, `pins`) supplies the defaults an explicit `gc()` call may override; it schedules nothing.
- The report carries `scannedBlobs`, `liveDigests`, `sweptDigests`, `reclaimedBytes`, `keptByGrace`, and `dryRun`.

`ArtifactSweep` in `@smthrs/artifacts` is the host-local deletion surface underneath. Its `inventory` lists only canonical fanout files, skipping temp files, lock files, and foreign paths, and `remove` fences deletion on `ifUnmodifiedSinceMs`, returning `false` when the blob is already gone, fails the age fence, or is protected by a live backup lease.

## Verify it worked

- `smthrs gc --older-than 30d --dry-run` lists the runs a real pass would delete and reports empty `deleted` counts; the real pass then deletes them, oldest first, across both databases.
- `smthrs runs list` no longer lists the collected runs. Live runs, and terminal runs with a live run above or below them in the lineage, are still listed.
- `ArtifactGc.gc({ dryRun: true })` names the digests a real collection would sweep; with a live backup lease, `ArtifactSweep.remove` returns `false` for those digests instead of deleting them.

## See also

- [`smthrs gc`](/docs/reference/cli/gc/): flags, output, and exit codes.
- [Project layout](/docs/reference/project-layout/): what each `.flows/` file holds.
- [Artifacts and the step cache](/docs/guides/artifacts-cache/): how blobs get into `.flows/objects` in the first place.

---

# Operate the memory store
https://smithers.sh/docs/guides/memory-ops/

Inspect and maintain the namespaced facts a project's flows remember, from the CLI and from scheduled maintenance effects.

The store itself (facts, threads, notes, and recall) is a concept; [Memory](/docs/concepts/memory/) covers it. This guide operates it.

## Before you start

- You have a project with a `.flows/control.db`, the control database the memory store lives in.
- You have facts to inspect, for example written by a flow that ran `Flows.remember`. For the maintenance step, `pnpm add @smthrs/memory@next effect@4.0.0-rc.112`.

## 1. Inspect facts

```bash
smthrs memory list --namespace flow:deploy-status
smthrs memory get deploy-target --namespace flow:deploy-status
```

- `list` returns up to 100 facts by default; use `--limit` to change the limit. Each entry includes `key`, `value`, and `updatedAtMs`. To narrow the listing, pass `--prefix deploy-`: only facts whose key starts with the prefix return.
- `get` returns the full `Fact` record: `namespace`, `key`, `value`, `provenance`, `createdAtMs`, `updatedAtMs`, and optional `tags` and `ttlMs`. A key the namespace does not hold exits 1.
- Without `--namespace`, every subcommand works in `user:cli`.
- `--json` selects machine-readable JSON output; `get` includes the full record with the stored value in its `value` field.

## 2. Set and delete facts by hand

```bash
smthrs memory set deploy-target staging --namespace flow:deploy-status
smthrs memory rm deploy-target --namespace flow:deploy-status
```

- A value that parses as JSON is stored as JSON; anything else is stored as the string as typed.
- `set` returns `{ "key": "deploy-target", "namespace": { "kind": "flow", "id": "deploy-status" }, "stored": true }` for the example above. Writing an existing key replaces its value.
- `rm` returns `{ "key": "deploy-target", "deleted": true }` if the fact existed, or `deleted: false` if it did not.

Reach for these to seed a fact before a run starts or to remove one afterwards. The command never reads or writes run history; `smthrs gc` handles that.

## 3. Choose the namespace

Select a namespace with `--namespace <kind:id>` or `--namespace <kind> --id <id>`, where the kind is `flow`, `agent`, `user`, or `global`. For example, `--namespace flow:deploy-status` and `--namespace flow --id deploy-status` select the same namespace. Do not combine `kind:id` with `--id`. An invalid kind, an empty id, an id containing control characters, or combining both spellings exits 1.

The kind is a stable lifetime, so pick it by who the fact belongs to:

- Stay in the default `user:cli` for your own notes and experiments.
- Write `flow:<id>` when you seed state for one flow tree. A flow's memory policy names the namespace that tree reads and writes, so match the id the run's policy uses.
- Write `global:<id>` for facts that belong to no single flow, agent, or user. The store's own fallback history thread lives at `global:history`.

## 4. Run maintenance

`@smthrs/memory`'s `Maintenance` module holds the finite maintenance effects, intended for explicit schedules. Nothing runs them on its own; you run a pass from a script over your store layer, or from a scheduled flow.

```ts
import { Maintenance } from "@smthrs/memory"
import * as Effect from "effect/Effect"

const pass = Effect.gen(function*() {
  const ttl = yield* Maintenance.ttlGc
  const history = yield* Maintenance.limitHistory({ maxTokens: 8192 })
  return { ...ttl, ...history }
})
```

What each pass does:

- `Maintenance.ttlGc` deletes facts whose TTL has elapsed. The expired fact, its full-text projection, and its vector rows go in one transaction. It returns `{ deletedFacts }`.
- `Maintenance.limitHistory` deletes the oldest messages in every thread until the configured approximate token budget is met: `maxTokens`, with `charsPerToken` defaulting to 4. It returns `{ deletedMessages }`.
- `Maintenance.compact` summarizes old history through an injected summarizer and atomically replaces the source messages with the summary, keeping the two most recent messages unless you pass `keepRecent`. The summarizer runs before the write transaction, so a failure or interruption leaves the source messages intact. It returns `{ compactedThreads, deletedMessages }`.

## 5. Back up memory with the rest of the project

Facts live in `.flows/control.db`, so the project backup in [Back up, gc, and retain run state](/docs/guides/retention/) covers the memory store.

## Verify it worked

- A `set` followed by `get` returns the full fact with the stored `value`, and `list` includes the fact with its `updatedAtMs` within the requested limit.
- After `rm`, a `get` on the same key exits 1.
- A maintenance pass returns its counts: `{ deletedFacts: 0, deletedMessages: 0 }` on a store with nothing expired and short threads.

## See also

- [Memory](/docs/concepts/memory/): facts, threads, notes, namespaces, and recall.
- [`smthrs memory`](/docs/reference/cli/memory/): subcommands, flags, and output.
- [Back up, gc, and retain run state](/docs/guides/retention/): the backup that covers `control.db`.
- [The memory tutorial](/docs/tutorials/memory/): remember and recall from inside a flow.

---

# Run flows in CI
https://smithers.sh/docs/guides/ci/

Run a flow from CI with `smthrs flow start`. A local attached invocation stays with the run and gives the job an exit status when it completes, fails, or parks.

This guide covers durable flows. `smthrs ci <pattern>` runs build, test, lint, and docs targets from `PACKAGE.ts`; it does not launch every flow in the repository.

Smithers Cloud does not run checks from repository events yet; [Run CI on Smithers Cloud](/docs/guides/cloud-ci/) lists the shipped paths and the planned service.

## Before you start

Commit your flow sources under `flows/` and configure their model seats. The runner needs Node.js 22.19.0 or later and the Smithers CLI. Store provider credentials in your CI system's secret store and expose only the variables the chosen seat needs.

A fresh checkout has no `.flows/` history. Work that needs to survive the job belongs on a persistent host.

## Add a GitHub Actions job

This job runs the existing `deploy/status` flow after a push to `main`. Replace that ID with your own flow. The example assumes its seat uses `ANTHROPIC_API_KEY`.

```yaml title=".github/workflows/smithers.yml"
name: Smithers flow
on:
  push:
    branches: [main]

jobs:
  flow:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install --global @smthrs/cli@next
      - name: Run the flow
        run: smthrs flow start deploy/status --format json
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      - name: Inspect runs after a failure
        if: failure()
        run: smthrs runs list --format json
```

The `@next` tag selects the release-candidate line. Pin a specific tested package version when you need repeatable CI installs. The timeout bounds the job; choose a value appropriate for your flow.

`flow start` approves its launch with run scope and does not prompt. Use it only for work the pipeline is authorized to run.

## Handle the exit status

For a local attached launch, these outcomes matter to the job:

| Code | Meaning | Response |
| --- | --- | --- |
| `0` | The run completed. | Continue the pipeline. |
| `1` | The command or run failed. | Read the diagnosis and events. |
| `2` | The invocation is invalid. | Fix arguments or malformed input. |
| `3` | The run is waiting for approval. | Apply the job's approval policy below. |
| `130` | Cancellation or SIGINT. | Treat the job as interrupted. |
| `143` | SIGTERM. | Check runner shutdown or timeout. |

A read command such as `runs show` can succeed while reporting a failed run. Its exit status reports the read, not the workflow outcome.

Use `--format json` for structured results and `--format jsonl` for event streams. Keep scripts on the canonical command groups; older flat aliases retain compatibility output contracts.

## Decide how the job handles approvals

Run-scope launch approval does not eliminate every possible in-run request. If the flow parks, choose one of these arrangements:

- **Keep the job unattended.** Treat exit `3` as a failed pipeline requirement and change the flow so it does not need a human response during that job.
- **Use persistent execution.** Run the work on a persistent control-plane host, submit the exact approval there, and resume the same run after the decision.

A remote launch returns an admission receipt rather than waiting for terminal completion. If your pipeline launches remotely, it must separately inspect that run and enforce its completion policy. A successful remote launch alone is not a successful workflow.

See [Run a control plane](/docs/guides/control-plane/) for remote connections, and [Plan, approve, and run](/docs/guides/plan-approve-run/) for the approval sequence.

## Keep useful failure evidence

Before the runner exits, inspect the run ID returned at launch:

```bash
smthrs runs show <run-id> --format json
smthrs runs logs <run-id> --limit 100 --format jsonl
```

Replace `<run-id>` with the actual ID. Save the output using your CI system's artifact facility if you need it after the job. Log exports help diagnosis; they are not a backup that can resume the execution.

For resumable state, follow [Back up and retain run state](/docs/guides/retention/). A new checkout cannot resume a run merely because the previous job printed its ID.

## Verify the job

Run the launch command locally against the same flow and credentials, then inspect its status. In CI, confirm that a failed or parked flow fails the job and that the diagnostic step still runs. [Inspect runs](/docs/guides/inspecting-runs/) explains how to narrow a failure to its recorded events and outputs.

---

# Test your flows
https://smithers.sh/docs/guides/testing-flows/

Exercise a flow in a test runner the way the shipped examples do: on the in-memory engine, asserting on what the run recorded.

## Before you start

- Node.js 22.19.0 or later. The durable engine runs on Node only.
- The library dependencies, with Effect pinned exactly:

```bash
pnpm add @smthrs/flow@next @smthrs/engine@next @smthrs/platform-node@next effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112
```

- The testing library and a runner as dev dependencies:

```bash
pnpm add --save-dev @smthrs/testing@next vitest@4.1.9 @effect/vitest@4.0.0-rc.112
```

`@smthrs/testing/Vitest` is ESM-only and deliberately absent from the root barrel, so import the adapter from its subpath.

- A module flow to test. [Write module flows](/docs/guides/module-flows/) covers authoring; this guide tests a flow in that style.

## 1. Run the flow on the in-memory engine

`FlowEngine.layerMemory` supplies an engine that keeps its state in the process, so a test needs no database and no control plane. Assemble the flow's layer the same way the runnable examples do:

```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"

export const Greet = Action.make("examples/Greet", {
  payload: { name: Schema.String },
  success: Schema.String
})

export const Greeting = Flow.make("examples/Greeting", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})

const GreetingLayer = Layer.mergeAll(
  Greet.toLayer(({ name }) => Effect.succeed(`Hello, ${name}.`)),
  Interpreter.layer(Greeting)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)

export const main: Effect.Effect<string> = Greeting.execute(
  { name: "Ada" },
  { executionId: "greeting-ada-1" }
).pipe(Effect.orDie, Effect.provide(GreetingLayer))
```

The `NodeCrypto` layer is required even in memory: an action dispatch is recorded under a derived step identity, so the engine needs a `Crypto`.

## 2. Assert on the recorded result

The test body yields the program and asserts on the value the flow returned. This is the whole skeleton, adapted from the shipped examples:

```ts
import { expect, it } from "@smthrs/testing/Vitest"
import * as Effect from "effect/Effect"
import { main } from "../src/greeting.ts"

it.effect("runs a typed flow on the in-memory engine", () =>
  Effect.gen(function*() {
    expect(yield* main).toBe("Hello, Ada.")
  }))
```

`it.effect` runs the Effect body under a test clock. `execute` fails typed when a payload does not satisfy the flow's schema, so schema validation is covered by feeding a bad payload and asserting the failure channel, for example with `Effect.flip`.

Beyond the return value, assert on what the run recorded. The durable examples read the journal back through the `Journal` service:

```ts
import { Journal, type JournalEvent } from "@smthrs/journal"

const journal = yield* Journal.Journal
yield* journal.flush
const page = yield* journal.entries({ runId: "build-1" as JournalEvent.RunId, limit: 200 })
const eventTypes = page.entries.map((entry) => entry.eventType)
```

That example's test asserts the recorded `eventTypes` list is non-empty. The retry example asserts on the dispatch record instead: `summary.dispatches` is `3` and `summary.attempts` is `[1, 2, 3]`, proving the flaky action ran three times before succeeding. The crash-and-resume example asserts `summary.readDispatches` is `1` after a resume, proving completed work was not re-dispatched.

## 3. Assert on journal entries

`JournalAssertions.expectJournal` is the fluent assertion vocabulary over a journal's entries, read in `entry.index` order. Feed it the entries from a journal read like the one above:

```ts
import { JournalAssertions } from "@smthrs/testing"

const journal = JournalAssertions.expectJournal(entries)

yield* journal.executed("examples/Greet")
yield* journal.executedInOrder(["examples/Greet", "examples/Publish"])
yield* journal.terminal("completed")
```

`executedInOrder` is a subsequence match: the keys in this relative order, with other entries allowed between and around them. Terminal outcomes are `completed`, `aborted`, `failed`, or `suspended`.

Steps and journaled external effects answer separately. `journal.effect(key)` asserts about the journaled effect entries under a key, and an ordinary step entry that happens to share the key never satisfies it: the assertion fails with `effect_kind_mismatch`. Under one key, `journaledAtMostOnce()` states the at-most-once journaling claim, and `exactlyOnce()` always fails, because an engine can prove at-least-once delivery and at-most-once journaling but never exactly-once external effect execution.

Every assertion failure carries a stable `code` from one closed union, exported as `TestingError.Code`. Match on the code, never on the prose of a message.

## 4. Record and replay model-backed steps

A flow whose steps call a model can run in tests without a provider. `@smthrs/testing` ships the record-and-replay loop:

- `RecordingModel` sits in front of a real model and records what the provider saw. It projects the request when the stream is acquired, not after it ends, and snapshots each event as it is emitted, so mutating your own request or event objects during an exchange does not change the recording.
- `CachedModel` replays what the fixture has and records what it is missing, keyed by the full canonical request with `modelId` included. Switching models is an ordinary miss that records a second entry.
- `RecordedModel` is the strict double. It matches by request shape with `modelId` erased, claims each recorded call once, refuses a request the fixture does not describe, and refuses a fixture recorded against another model (`REPLAY_HARNESS_MISMATCH`). Neither model double reads the environment, so how a suite decides to record stays the suite's business.

A refusal is a defect rather than a typed error, and it carries a bounded identity: the model id, the message count, and the tool names, never the conversation.

To assert replay counts, read the controller: `RecordedModel.unconsumed()` returns the fixture calls not yet selected for replay. Expecting it to be empty at the end of a test proves the flow made exactly the recorded calls and no more.

## 5. What to assert at each layer

| Layer | Assert | With |
| --- | --- | --- |
| Schema validation | `execute` fails typed on a payload that misses the schema | `Effect.flip` on the failing call |
| Plan shape | Node count, node ids, edges, keys, placements, declared effects, canonical rendering | `PlanAssertions.expectPlan` |
| Plan purity | Plan-time computation never touches the host, the model, the clock, or randomness | `PlanAssertions.expectPure` under `TestLayers.poisoned`, which reports escapes as `purity_violation` |
| Recorded outputs | Return value, dispatch and attempt counts, journaled step and effect entries, terminal outcome | vitest `expect`, `JournalAssertions.expectJournal` |
| Replay counts | Every recorded model call consumed, no unscripted call attempted | `RecordedModel.unconsumed()` |

Two plan assertions earn their keep over time. `PlanAssertions.expectKeyGoldens` pins step keys as golden literals, so a change in how a step is keyed turns into a red test rather than a silent cache miss. `PlanAssertions.expectPlans(...).covers(...)` asserts that a static suite of built plans reaches each requested node id.

Race and interrupt cases advance virtual time: register them through `Vitest.testEffect(...).effect`, which supplies a deterministic clock, and not through `.live`, which does not.

## Verify it worked

```bash
pnpm vitest run
```

Each `it.effect` body runs its flow to completion in process. A failing assertion prints the typed error with its stable code, so a red suite names what broke (for example `effect_kind_mismatch`) instead of dumping the journal.

## See also

- [Write module flows](/docs/guides/module-flows/): the authoring model these tests exercise.
- [Durable execution](/docs/concepts/durable-execution/): the journal the assertions read.
- [Content addressing](/docs/concepts/content-addressing/): the step identity golden key vectors pin.
- [API reference](/docs/reference/api/): the generated listing of every `@smthrs/testing` module and export.

---

# Review pull requests with the Smithers action
https://smithers.sh/docs/guides/pr-review-action/

Get an agent review with a narrated HTML walkthrough on your pull requests, without running inference yourself.

The Smithers review service runs one review agent per changed file, then a narrator agent writes a walkthrough of the whole change: chapters in logical reading order, prose explaining why each group of files changed, diffs embedded at the right point in the narrative, and Mermaid diagrams wherever structure changed. The output is a single self-contained HTML file you can open, share, or publish to a hosted URL.

Pointed at a GitHub pull request, it also posts the review onto the PR: the narrative summary as the review body, and every finding as an inline comment with a `suggestion` block when there is replacement code to apply.

Findings never fail the build. The review service reports; humans decide.

This guide covers the review action and the separate `smithers-review` executable.
The `smthrs review <pattern>` command executes workspace model-review targets;
use `smithers-review` for the pull-request service described here. See the
[CLI reference](/docs/reference/cli/) for target execution.

## Before you start

- Register your repository. v0 accounts are operator-issued while billing is built out: open an issue titled `review access: <org>/<repo>` on [smithersai/smithers](https://github.com/smithersai/smithers/issues) or contact the maintainers.
- No secrets, no Anthropic account, no smithers checkout are needed. The service authenticates your repository through GitHub OIDC, runs the agents on its metered inference, posts the review, and hosts the walkthrough.

## 1. Add the workflow file

Create `.github/workflows/smithers-review.yml`:

```yaml
name: smthrs review
on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]
  issue_comment:
    types: [created]

permissions:
  id-token: write       # proves your repo's identity to the review service
  contents: read        # check out the PR
  pull-requests: write  # post the review

concurrency:
  group: smithers-review-${{ github.event.pull_request.number || github.event.issue.number }}
  cancel-in-progress: true

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: smithersai/smithers/apps/review/action@main
```

The three permissions are the whole set the action needs: `id-token: write` proves your repository's identity to the review service, `contents: read` checks out the PR, and `pull-requests: write` posts the review.

:::caution
Keep the workflow on `pull_request`. Never switch it to `pull_request_target`: the review agents execute the PR's code, and `pull_request_target` would hand that code elevated credentials.
:::

## 2. Trigger a review

Comment on any pull request:

```text
@smthrs review
```

Only owners, members, and collaborators can trigger reviews. Repositories registered in `auto` mode skip the comment and review every non-draft PR push; `comment` mode is the default. The mode is a server-side setting on your registration, so switching never touches your workflow file.

Subscriptions meter reviewed PRs as a monthly per-repo allotment, and the status comment shows remaining quota. Re-reviewing a PR that already counted this month is free. When the quota is spent, the action skips with a notice instead of failing your checks.

## 3. Pay for your own inference (optional)

To pay for the review's inference directly instead of using the service's metered inference, set `ANTHROPIC_API_KEY` as a repository secret and pass it through in the job:

```yaml
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    steps:
      - uses: smithersai/smithers/apps/review/action@main
```

`OPENAI_API_KEY` works the same way and moves both seats onto the `openai:` provider. The action prefers Anthropic when both are set, and it scrubs the raw secret out of the environment before the review starts: the run reads an untrusted diff, so it is handed only the credential its chosen mode needs. Repository registration, quota counting, and walkthrough hosting work exactly as before; only the inference moves to your key.

`CODEX_AUTH_JSON` and `CLAUDE_CODE_OAUTH_TOKEN` are ignored. Those were the 0.x subscription modes; this release runs no CLI subprocess, so the credential is an API key and there is nothing to materialize on disk.

## 4. Run the same review from a terminal

The review package ships `smithers-review`, which runs its review flow against a repository. This is separate from `smthrs review`, which selects review-kind targets from `PACKAGE.ts`:

```bash
# review the working tree of a repo, write .smithers-review/walkthrough.html
smithers-review /path/to/repo

# review a branch against main, open the walkthrough when done
smithers-review /path/to/repo --from main --to HEAD --open

# review one commit
smithers-review /path/to/repo --commit abc1234

# review GitHub PR #123 and post the review onto it (via gh)
smithers-review /path/to/repo --pr 123

# publish the walkthrough to the share service and print an unlisted URL
smithers-review /path/to/repo --pr 123 --publish

# no seats: deterministic story, no review findings (works offline)
smithers-review /path/to/repo --no-review --no-narrate
```

The repository path defaults to the current directory. `--publish` needs a publish service URL in `SMITHERS_REVIEW_PUBLISH_URL` and an operator-issued API key in `SMITHERS_REVIEW_PUBLISH_TOKEN`; both can also be set in `~/.smithers-review.json`.

## Configuration knobs

Seats are `provider:model` strings, and the provider ahead of the colon decides which credential is read:

| Variable | What it sets |
| --- | --- |
| `SMITHERS_REVIEW_SEAT` | Reviewing and verifying seat. Default `anthropic:claude-sonnet-4-5`. |
| `SMITHERS_REVIEW_CHEAP_SEAT` | Narrating and quizzing seat. Default `anthropic:claude-haiku-4-5`. |
| `SMITHERS_REVIEW_VERIFY_SEAT` | Overrides the verifying seat alone. |
| `SMITHERS_REVIEW_NARRATE_SEAT` | Overrides the narrating seat alone. |
| `SMITHERS_REVIEW_QUIZ_SEAT` | Overrides the quizzing seat alone. |
| `ANTHROPIC_API_KEY` | Credential for `anthropic:` seats. |
| `OPENAI_API_KEY` | Credential for `openai:` seats. |
| `OPENROUTER_API_KEY` | Credential for `openrouter:` seats. |
| `ANTHROPIC_BASE_URL` | Sends `anthropic:` seats to a proxy origin instead of `api.anthropic.com`. |

The `quiz` action input, and `--quiz on` / `--quiz off` on the bin, control the reviewer quiz: a short set of multiple-choice questions a reviewer can answer only after reading the change. It auto-triggers when the assessed impact is high or critical, and the walkthrough lists the reasons behind the assessment. Nothing blocks a merge on the score.

## Verify it worked

Comment `@smthrs review` on an open pull request. The action posts the narrative summary as the review body and every finding as an inline comment, the status comment shows remaining quota, and the service hosts the walkthrough HTML. Because findings never fail the build, a review full of findings still leaves the check green; a spent quota skips with a notice instead.

## See also

- [Run flows in CI](/docs/guides/ci/): the Smithers CLI's own CI contract, with exit codes and secrets.
- [Model seats](/docs/guides/model-seats/): what a `provider:model` seat string declares.

---

# Run CI on Smithers Cloud
https://smithers.sh/docs/guides/cloud-ci/

Smithers Cloud does not yet run your checks when code changes. This page states what runs `PACKAGE.ts` checks today and records the planned event-driven Cloud CI so that the design is visible before it ships. The planned material sits under the [Planned](#planned-event-driven-cloud-ci) heading and nothing in it is available.

## What runs checks today

Three shipped paths run the targets declared in `PACKAGE.ts`:

| Path | What it does | Where it is documented |
| --- | --- | --- |
| `smthrs ci <pattern>` | Runs build, test, lint, and docs targets as one graph on the machine you run it on, with normal cache reuse. | [Build targets](/docs/guides/build-targets/) |
| `Smithers.GithubCiGen` | A target that generates the GitHub Actions workflow files from declared attrs. GitHub Actions then runs `smthrs ci` on push and pull request events. | [`@smthrs/targets`](/docs/reference/api/targets/) |
| `smthrs flow start` in a CI job | Launches a durable flow from GitHub Actions and gives the job an exit status when the run completes, fails, or parks. | [Run flows in CI](/docs/guides/ci/) |

To start a flow on a schedule instead of from a CI job, register a cron trigger and host the scheduler with `smthrs serve` on a machine you control. See [Define triggers](/docs/guides/triggers/). Smithers Cloud workspaces do not host that scheduler yet.

If you are replacing a hand-written GitHub Actions workflow, generate the replacement with `Smithers.GithubCiGen`, verify one passing and one failing run, and then delete the old workflow. Required status checks keep pointing at GitHub Actions.

## Planned: event-driven Cloud CI

This section describes work that is not built. `@smthrs/targets` exports no `S.Automation`, `S.Package` accepts no `automations` entry, and Smithers Cloud registers no repository subscriptions. The type-level proof of the intended declaration surface is [`apps/site/examples/cloud-triggers/contract.d.ts`](https://github.com/smithersai/smithers/blob/main/apps/site/examples/cloud-triggers/contract.d.ts); its README states that it does not implement trigger registration, matching, dispatch, or the `S.Automation` runtime.

The intended shape declares a suite and binds it to repository events in `PACKAGE.ts`:

```ts title="PACKAGE.ts (planned; does not compile against the released packages)"
import { Smithers as S } from "@smthrs/targets"
import { Trigger as T } from "@smthrs/triggers"
import { Package as src } from "./src/PACKAGE.js"

const checks = S.Suite({
  tests: [src.lint, src.typeCheck, src.test],
})

const ci = S.Automation({
  on: [T.push({ branches: ["main"] }), T.review({ base: ["main"] })],
  run: checks,
})

export const Package = S.Package({
  targets: { checks },
  automations: { ci },
})
```

The planned service would register the declarations from the default branch or bookmark when you enable CI for a connected repository, start a run on each matching push or review event, execute the target graph in an isolated worker with normal cache reuse, and publish a **Smithers CI** result on the tested commit through the Smithers GitHub App or on the Plue landing request, so that the result could be a required check. A newer source revision on a review would cancel its earlier unfinished run. The [Cloud CI reference](/docs/reference/cloud-ci/) records the planned defaults, result meanings, and revision rules under the same disclaimer.

None of that runs today. GitHub Actions remains the shipped way to run checks on repository events.

---

# Define triggers
https://smithers.sh/docs/guides/triggers/

A trigger is a durable rule that starts a flow. The rule that ships today is a cron schedule: `smthrs triggers register` stores it in the project's control database, and the scheduler inside `smthrs serve` or `smthrs triggers serve` fires it while that process runs. Every fire goes through Control, so a flow that requires approval still waits for one.

Repository events (`push`, `review`, `tag`), typed manual input, external events, and completion chaining are not available yet. The [Planned](#planned-repository-events-and-packagets-declarations) section at the end of this page describes that design and marks it as unshipped.

You need a Smithers project with at least one flow. `smthrs flow list` prints the flow ids the trigger can name.

## Register a schedule

Give the trigger an id, the flow it runs, and a five-field cron expression:

```bash
smthrs triggers register nightly-lint --flow lint --cron "0 9 * * 1-5" --timezone America/New_York
```

The command prints the stored row as JSON. A first registration is revision 1; registering the same id again replaces the row and increments the revision.

Registration validates the schedule before it writes anything. A malformed expression fails with `invalid_cron`. An expression the calendar never satisfies, such as `0 0 30 2 *`, fails with `unsatisfiable_cron`; the check searches forward from now for one occurrence, the same search every scheduler tick performs. `--timezone` accepts an IANA name and defaults to UTC.

Pass fixed input for the flow as JSON with `--input '{"branch":"main"}'`. The input must survive `JSON.stringify` unchanged: `undefined`, `NaN`, a `Date`, or a function fails with `invalid_trigger` and names the `input` field.

To keep the declaration in the repository, write it to a file and register from it:

```json title="triggers/nightly-lint.json"
{
  "id": "nightly-lint",
  "flowId": "lint",
  "input": {},
  "cron": "0 9 * * 1-5",
  "timezone": "America/New_York",
  "overlap": "skip",
  "catchUp": "none",
  "maxCatchUp": 0,
  "enabled": true
}
```

```bash
smthrs triggers register nightly-lint --file triggers/nightly-lint.json
```

The `id` in the file must match the id on the command line; a mismatch fails and writes nothing. Registering from a file does not watch the file: rerun the command after editing it.

## Choose overlap and catch-up policies

Two policies decide what happens when an occurrence is due at an awkward time. Both default to the conservative choice.

| Policy | Values | Behavior |
| --- | --- | --- |
| `--overlap` | `skip` (default), `buffer-one`, `supersede` | What to do when an occurrence is due while the previous run is still active. `skip` records the occurrence and starts nothing. `buffer-one` keeps one pending occurrence and fires it when the active run settles; a second due occurrence coalesces into the same buffer. `supersede` cancels the active run and starts the new occurrence. |
| `--catch-up` | `none` (default), `one`, `all` | What to do with occurrences missed while no scheduler was running. `none` ignores them. `one` fires the most recent missed occurrence. `all` fires every missed occurrence in order. |
| `--max-catch-up` | `0` (default) to `1000` | The most missed occurrences a catch-up may replay. With the default `0`, `one` and `all` replay nothing. A backlog beyond the bound is abandoned with a warning, and the current occurrence still fires. |

Disabling a trigger never cancels its active run. Only a `supersede` claim cancels a run.

## Host the scheduler

Schedules fire only while a scheduler process runs against the same project:

```bash
smthrs serve
```

`smthrs serve` hosts the control server and the trigger scheduler on one Control service. For a scheduler without the HTTP server, run `smthrs triggers serve`; `--poll-interval-ms` sets the tick period and defaults to 1000.

On each tick the scheduler computes which enabled triggers are due, claims each occurrence in the store, and starts the flow through Control with the idempotency key `<triggerId>:<occurrence ISO instant>`. Two hosts that notice the same boundary derive the same key, so a retry or a second scheduler cannot start the same occurrence twice.

Stopping the scheduler cancels nothing. Launched runs are durable and outlive the process; the next scheduler re-attaches to them from the store.

## Resolve a parked approval

A flow that requires approval parks its plan. The CLI scheduler in `smthrs serve` and `smthrs triggers serve` uses the durable `TriggerPlans` runner. Parked plans remain active across scheduler restarts with the same approval payload and no attempt counter or expiry. The scheduler never approves a plan.

List the triggers, inspect the waiting plan, and approve it from the CLI:

```bash
smthrs triggers list
smthrs triggers show nightly-lint
smthrs approvals approve '<activePlan.plan.approval>' --scope run
```

`show` prints `activePlan.plan.approval` when a plan is waiting. Submit that exact JSON payload unchanged. To cancel a parked launch, deny its approval instead:

```bash
smthrs approvals deny '<activePlan.plan.approval>'
```

The scheduler observes the denial on its next check and marks the stored plan `failed` without launching a run. Disabling the trigger or stopping the scheduler does not cancel its parked plan. See the [Trigger reference](/docs/reference/triggers/#scheduler) for scheduler claims and overlap behavior.

:::note[Library embedders only]
When a library embedder installs `Scheduler.layerControlRunner`, a parked plan has eight attempts with a delay that doubles from one second. The last attempt lands a little over two minutes after the first; if approval is still missing, the occurrence is recorded as `failed` with error `runner`. This limit applies only to that optional library runner.
:::

## Fire an occurrence by hand

Queue one occurrence for a running scheduler:

```bash
smthrs triggers fire nightly-lint
```

The command records a pending occurrence stamped with the current time and prints its idempotency key. The scheduler picks it up on its next tick and applies the trigger's overlap policy exactly as it would for a cron boundary. A disabled trigger refuses to queue; enable it first. `--occurrence <unix-ms>` sets the occurrence instant so a retried command reuses the same key instead of queueing a second fire.

## Inspect triggers and the fire ledger

`smthrs triggers list` prints every registration ordered by id, each with its `revision`, `enabled` flag, and `lastFiredAt` once an occurrence has been recorded. `smthrs triggers show <id>` adds `activeRun`, the run id or launch reservation the trigger holds right now, and `activePlan` when that run is waiting for approval.

Every occurrence ends with one recorded outcome:

| Outcome | Meaning |
| --- | --- |
| `launched` | Control accepted the run and it is still active. |
| `completed` | The run settled. |
| `skipped` | The occurrence was due while a run was active and the policy is `skip`. |
| `buffered` | The occurrence is waiting behind an active run under `buffer-one`. |
| `superseded` | The occurrence cancelled the active run under `supersede`. |
| `failed` | The runner could not prepare or launch the flow. |

`lastFiredAt` is the cursor catch-up resumes from. It advances on skipped and buffered occurrences too, so it does not tell you which occurrence an active run owns; `show` does.

`smthrs triggers enable <id>` and `smthrs triggers disable <id>` flip future occurrences without touching an active run. Both increment the revision.

## Read triggers in the app

Signed in to Smithers, run `/triggers.list` or ask "list the triggers on owner/repo". The Dispatcher card prints one row per trigger: the schedule in words, the flow it runs, whether it is enabled, and when it last fired.

The card reads the workspace's gateway. When that gateway does not serve its trigger store, the card prints the gateway's own reason instead of an empty list, and never a made-up row. Smithers Cloud workspaces do not serve the store yet, so today the card reports that and points you at `smthrs triggers list` on the host that runs the scheduler. Registering and editing triggers from the app is not available; use the CLI.

## Accept a verified webhook

`@smthrs/triggers/Webhook` builds a webhook door in code. `Webhook.make` takes a channel configuration and a required credential reference, verifies the request signature over the raw bytes in constant time, and hands the decoded payload to Control's channel coordinator. It exposes `register` and `ingest` and no direct execution method, so a valid signature and valid JSON never bypass Control.

There is no `smthrs webhooks` verb and no webhook row in the app yet. For the module contract, see [`@smthrs/triggers`](/docs/reference/api/triggers/#webhook). For GitHub webhooks that reconcile against `.smithers/listeners.json`, see `smthrs integrations reconcile` in the [CLI reference](/docs/reference/cli/).

## Planned: repository events and PACKAGE.ts declarations

This section describes work that is not built. Nothing in it is importable today: `@smthrs/targets` exports no `S.Automation`, `S.Package` accepts no `automations` entry, and `@smthrs/triggers` exports no `Trigger.push`, `review`, `tag`, `schedule`, `manual`, `event`, or `succeeded` constructor. The type-level proof of the intended surface lives in [`apps/site/examples/cloud-triggers/contract.d.ts`](https://github.com/smithersai/smithers/blob/main/apps/site/examples/cloud-triggers/contract.d.ts), whose README states that it implements no registration, matching, dispatch, or `S.Automation` runtime.

The intended shape binds typed event selectors to an existing target or flow inside `PACKAGE.ts`:

```ts title="PACKAGE.ts (planned; does not compile against the released packages)"
import { Smithers as S } from "@smthrs/targets"
import { Trigger as T } from "@smthrs/triggers"

const ci = S.Automation({
  on: [T.push({ branches: ["main"] }), T.review({ base: ["main"] })],
  run: prePush,
})

export const Package = S.Package({
  targets: { prePush },
  automations: { ci },
})
```

The planned selectors are `T.push` with branch and path filters, `T.review` for GitHub pull requests and Plue landing requests, `T.tag`, `T.schedule` as the declared form of today's cron trigger, `T.manual` with a typed input schema, `T.event` for authenticated external publishers, and `T.succeeded` to chain one automation after another. Filters combine with AND, list values with OR, and an exclusion always wins. The [Trigger reference](/docs/reference/triggers/) records the planned event fields and matching rules under the same disclaimer.

Until that lands, the shipped way to start work from a repository event is a CI job: [Run flows in CI](/docs/guides/ci/) launches a flow from GitHub Actions, and the [`Smithers.GithubCiGen` target](/docs/reference/api/targets/) generates the workflow files that run `smthrs ci`.

---

# How target caching works
https://smithers.sh/docs/concepts/target-caching/

A cached target reuses a previous result instead of executing its command. Smithers derives its key from the declaration, file inputs, dependencies, and execution environment. A cache hit is valid only when those inputs describe everything that can affect the result.

This page covers `PACKAGE.ts` targets. The durable engine's [step cache](/docs/guides/artifacts-cache/) is a separate API for sharing eligible action results.

**A target cache lookup**

Smithers derives a key from declared work and its environment, then looks it up. A hit restores the recorded result and declared outputs. A miss executes the target and records its result.

## What changes the key

The key covers more than source-file bytes:

| Material | Examples |
| --- | --- |
| Target body | Rule identity, effective mode, working directory, declared output roots. |
| Inputs | Attributes, expanded file inputs, and dependency keys. |
| Ambient environment | Node version, platform, architecture, lockfile, and executor implementation fingerprint. |
| Resolved layers | Environment declarations such as a Nix closure. |
| Capabilities | The authority declared for the target. |

A changed dependency also changes its consumers' keys. An independent target can keep its cached result. Changes to the executor or shared environment can invalidate many targets at once.

## Inputs are an authoring contract

Declare the files and dependencies the target reads. A file hidden from the declaration can change without changing the key. Likewise, a command whose result depends on an unrepresented external service may need caching disabled or a rule that explicitly models that dependency.

The [first target tutorial](/docs/tutorials/first-target/) demonstrates a file edit that causes a miss. [Run selected and affected targets](/docs/guides/build-targets/) explains how change selection and execution fit together.

## Results and output bytes

The target cache records results and captures declared `outDir` trees or `outFile` files into its content-addressed artifact store. During restoration, it checks blob digests and confines restored paths to the target's declared outputs.

The workspace cache directory comes from `--cache-dir`, then the workspace's `S.Cache({ directory })`, then `.flows`. Keep it out of version control. Its contents are excluded from input discovery.

## Local and remote lookup

A local hit avoids a network request. When configured, a remote hit fills the local cache for later reads. Remote failures warn and disable the remote for the rest of that process; the build can continue with local execution.

Separate read access from publication credentials when untrusted jobs can consume trusted results but must not publish them. [Configure a shared target cache](/docs/guides/remote-cache/) in the workspace declaration. The [build CLI package reference](/docs/reference/api/build-cli/) links to its complete workspace and transport contract.

## Diagnose an unexpected result

Start with the exact target:

```bash
smthrs explain '//apps/site:build'
smthrs build '//apps/site:build' --plan
```

Replace the label with one in your workspace. Check these causes before clearing anything:

| Symptom | Check |
| --- | --- |
| A source edit still hits the cache | Is the file included in the target's declared inputs? |
| The same code misses on another machine | Do Node version, platform, architecture, lockfile, and environment match? |
| Many targets miss after a tool upgrade | Did the executor implementation fingerprint change? |
| A result restores without an expected file | Is the file inside the target's declared output roots? |
| The remote is unavailable | Read the warning; confirm local execution still completes. |

`--no-cache` bypasses reads for one invocation and still publishes its result. Use it to compare fresh execution with a cached result. Cache deletion alone does not repair a missing input declaration.

---

# Durable execution
https://smithers.sh/docs/concepts/durable-execution/

A durable run keeps its progress after the process running it stops. When you resume the same execution, Smithers reuses recorded action outcomes and continues the unfinished work. The run's SQLite state must still be available.

The useful boundary is a **recorded action outcome**. Code outside that boundary can run again, including the surrounding implementation that calls the action.

**Resume reuses committed outcomes**

The first process records action A, then stops during B. A fresh process opens the same run state, reuses A's outcome, and executes unfinished B.

## What survives a restart

Smithers stores run state, action attempts, and lifecycle events in SQLite. The journal orders events within a run. The run and attempt stores hold the state the engine needs to continue execution.

When an action settles, `Journal.transact` commits its state change and lifecycle event together. Both commit or both roll back. Subscribers receive the event after the transaction commits.

This prevents a history entry from claiming that a step completed when its outcome was never stored. It also prevents a stored completion from losing the event that explains it.

## What runs again

On resume, the engine starts from the flow definition and looks up recorded outcomes as it reaches action boundaries. A completed action returns its stored outcome. A recorded failure returns its stored failure. Unfinished work can execute again.

For example, suppose an implementation increments a local counter, calls a recorded `ReadDocument` action, and then waits for approval. After resuming:

- The implementation enters again, so the local counter increments again.
- `ReadDocument` returns its recorded result without reading the document again.
- The wait reads the recorded approval and continues.

The [crash and resume tutorial](/docs/tutorials/crash-and-resume/) makes these boundaries visible with counters.

:::caution[External effects can happen twice]
A remote service can accept a request before Smithers records the response. If the process stops between those events, resuming can repeat the request. Use the remote service's idempotency mechanism for effects such as payments or publishing. A local transaction cannot make a remote request atomic.
:::

## Resume, replay, and cache reuse

These operations use stored data for different purposes:

| Operation | Purpose | Can execute action code? |
| --- | --- | --- |
| Resume | Continue an existing execution from its recorded state. | Yes, for unfinished work. |
| Time-travel replay | Read a committed prefix of history and derive a view. | No. |
| Step-cache lookup | Reuse an eligible sealed result by its key. | A miss can lead to execution. |

Use [time travel](/docs/concepts/time-travel/) to inspect history. Use [artifacts and the step cache](/docs/guides/artifacts-cache/) to share eligible results between runs.

## Lifecycle events and telemetry

Lifecycle events take the durable journal channel. Telemetry uses a bounded queue and can be dropped under pressure. The distinction lets the engine keep execution records without allowing diagnostic traffic to grow without a bound.

Journal readers also choose a contract. `changes` provides bounded live notifications; a slow subscriber can miss entries. `stream` reads durable history and follows committed changes. Use the durable stream when every event matters.

The [journal reference](/docs/reference/api/journal/) describes transactions, producer identities, and duplicate-write handling.

## Keep the state that makes resume possible

The CLI stores run state under `.flows/`. A new checkout without that directory has no local history to resume. Put long-running work on a persistent host, and back up its state using the [retention guide](/docs/guides/retention/).

Keep execution identity stable when resuming and use a new identity for unrelated work. [Execution IDs and ownership](/docs/concepts/ownership/) explains how the engine distinguishes runs and prevents two owners from writing the same run.

---

# Flows, actions, and plans
https://smithers.sh/docs/concepts/flows-actions-plans/

An **action** declares one operation and its input and output types. A **flow** composes operations into a graph. A **plan** records that graph and the keys used to identify its work.

The control plane admits work in three stages: build a plan, approve its digest, then launch a run. Separating these stages lets a person or policy review the work before it executes.

**From declarations to execution**

A flow body describes action calls. Planning records the graph and its digest. Approval binds to that digest, and an engine executes the approved work using registered implementations.

## Declare work separately from its implementation

The declared form of `Action.make` carries schemas and a tag. `toLayer` attaches its implementation:

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

const Greet = Action.make("example/Greet", {
  payload: { name: Schema.String },
  success: Schema.String,
  tier: "sealed"
})

const greetLayer = Greet.toLayer(({ name }) =>
  Effect.succeed(`Hello, ${name}.`)
)

const Greeting = Flow.make("example/Greeting", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})
```

This code defines work; it does not run it. `Greet.call(payload)` places an action call in the graph. The greeting string is produced later, when an engine executes the call with `greetLayer` installed.

The [first flow tutorial](/docs/tutorials/first-flow/) adds the interpreter and engine needed to run this pattern. The [flow reference](/docs/reference/api/flow/) also documents the executable action form and built-in system actions.

## Plan a round before running it

`Graph.build` evaluates the flow body symbolically. It records dependencies and branch structure before executing actions. A planned value represents a future result; ordinary JavaScript arithmetic or string conversion cannot read that result during planning.

Keep I/O in action implementations. A flow body describes how results connect. It does not read files or call models to discover the next node.

The complete graph is known for each round. A flow can settle with a handoff or another bounded round, so this does not mean every node of a long-running, dynamically elaborated workflow exists at its initial launch.

## Keys and approval digests

A node key depends on its declared material and material dependencies. The plan digest covers the graph's identities, keys, edges, and effects. [Content addressing](/docs/concepts/content-addressing/) explains which changes affect those keys.

The persisted plan grows by appending work. Its `baseDigest` identifies generation zero, the plan initially approved. Its current `digest` advances as elaborations append to the plan. Recorded nodes and edges are not edited in place.

The control-plane guide shows how to [review and submit the exact approval payload](/docs/guides/plan-approve-run/).

## Invalid plans fail before execution

Planning rejects ambiguous or incomplete graphs instead of executing their valid-looking parts. Common causes include duplicate node IDs, missing dependencies, dependency cycles, and conflicting declared write sets under a failing conflict policy.

At execution time, `unresolved_action` means an action has no registered implementation. `duplicate_node_id` identifies a dispatch-address collision, and `incomplete_graph` identifies topology the graph builder could not complete.

Use the [plan reference](/docs/reference/api/plan/) for compiler refusal codes and the [module flow guide](/docs/guides/module-flows/) for authoring patterns.

---

# Content addressing
https://smithers.sh/docs/concepts/content-addressing/

A content address identifies a value by its contents. Smithers uses content-derived keys to decide whether a stored result belongs to the work being requested. When declared material changes, its key changes, along with the keys of work that consumes it.

**Deriving a content key**

Declared material and dependency keys become canonical JSON. SHA-256 hashes those bytes into a key. A changed input changes the key used to look up a result.

## Derive a key

`@smthrs/keys` encodes an input as canonical JSON, hashes its UTF-8 bytes with SHA-256, and prefixes the digest with `key1_`:

```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, input: "main.ts" }).pipe(
    Effect.provide(NodeCrypto.layer)
  )
)
console.log(key)
// key1_ followed by 64 lowercase hexadecimal characters
```

Object property order does not change the canonical bytes. Values without an accepted canonical representation, such as cycles or non-finite numbers, fail key derivation.

The `domain` and `version` fields in this example are caller-owned material. They distinguish this protocol from other protocols that might otherwise hash the same input.

## Which changes affect identity

A plan node's key includes its material dependencies. Changing a dependency changes the keys of consumers downstream of it. Independent nodes can retain their keys.

Node lookup IDs and declaration tags have different roles. A structural node ID is an address in the graph. An action or flow tag also appears in the call's body material. Renaming a declaration can therefore change a call key; do not treat every name as cosmetic.

Call material also includes the declaration's payload, success, and error schemas as JSON Schema documents. This captures changes to the described data shape, with an important limit: two codecs can describe the same JSON Schema while decoding values differently.

:::caution[Identity only covers represented changes]
Changing a codec's behavior without changing its JSON Schema does not automatically change the call key. Give a changed declaration a new identity when previously recorded results must no longer apply. A content key cannot detect an input or behavior that its material does not represent.
:::

## Which work can share results

Only eligible `sealed` work can use content keys for reuse beyond its run. Compensable, irreversible, and unsealed work uses run-local identity instead. Declaring a step sealed is a contract about its behavior; it does not make arbitrary side effects safe to repeat or cache.

Cross-run cache reuse also requires the identity and environment declarations described in [Share results with artifacts and the step cache](/docs/guides/artifacts-cache/). A deterministic-looking action alone is insufficient.

## Stored keys and failures

Treat a stored key as opaque. `StoredKey` validates supported wire forms without hashing. The current form is `key1_` followed by 64 lowercase hexadecimal characters; an unknown version is rejected.

Key derivation reports `canonicalization_failed` when the input cannot be encoded, or `digest_failed` when the hash operation fails. Derivation holds the input and encoded document in memory, so callers accepting untrusted data need their own size bounds.

See the [keys reference](/docs/reference/api/keys/) for the API and the [canonical reference](/docs/reference/api/canonical/) for accepted values.

---

# Execution IDs and ownership
https://smithers.sh/docs/concepts/ownership/

An execution ID identifies a piece of work. Ownership identifies the process currently allowed to write that execution's state. The execution ID stays stable when a run resumes; ownership can move to a new process.

## How an execution gets its ID

Smithers selects an ID in this order:

| Priority | Source | When it applies |
| --- | --- | --- |
| 1 | `executionId` passed to `execute` | The caller supplies an explicit ID. |
| 2 | The flow's `idempotencyKey`, combined with its tag | The declaration supplies an identity. |
| 3 | `CurrentExecutionIds` | Neither explicit source is present. |

The default `CurrentExecutionIds` source, `fresh`, creates a cryptographic UUID for each unkeyed invocation. Equal tags and payloads start independent executions. Keep the returned ID when you need to resume that work after a crash.

When equal tags and payloads deliberately identify the same work, opt into `Flow.layerExecutionIds(Flow.derived)`, or declare an `idempotencyKey` on the flow. The derived source hashes the flow tag and encoded payload, so repeating that invocation reattaches to its execution. A custom source can be installed with `layerExecutionIds`.

An explicit ID and a declared idempotency key take precedence over the host-wide source. Reuse the ID to resume the same work; choose a new ID to start unrelated work.

## What ownership protects

An owner has a host ID, process ID, and nonce. State writes compare that complete identity with the owner recorded on the run. A successful claim therefore does not grant indefinite write access: every later write must still pass the check.

**A replacement owner fences out the old process**

Worker A holds a lease and sends heartbeats. After its lease becomes eligible for takeover, worker B claims the run. A later write from A fails because the stored owner is now B.

Attempt state and journal lifecycle writes use the same ownership check. A stale journal writer receives `fence_lost`; an absent or malformed owner instead produces `invalid_event`.

These checks protect writes through the stores. They cannot prevent an old process from making an unrelated remote request. External services still need idempotency or fencing when duplicate effects matter.

## Recovery after a process stops

A hard kill can leave a run marked `running`. The durable driver's sweep checks stale runs and attempts recovery when the ownership evidence permits it. Takeover rechecks the stored status, owner, and heartbeat before changing the row.

A fresh lease or positive liveness probe prevents takeover. When a replacement succeeds, a paused old process cannot resume writing under its old identity. Its next fenced write fails and the driver interrupts it.

Recovery requires a running driver with access to the same persistent state. A deleted database or an unavailable host cannot recover itself.

## Host and clock requirements

A PID is meaningful only on its own host. Use a same-host PID probe for local processes; a deployment spanning hosts needs a liveness policy appropriate to that deployment.

Lease operations also check clock skew and preserve monotonic heartbeats. Keep the lease clock and lifecycle clock consistent in a custom host composition. The [run-store reference](/docs/reference/api/run-store/) documents the claim protocol, refusal results, and timing options.

If the opt-in derived source cannot encode the payload canonically, it raises the defect `ExecutionIdRequired`. Validate inputs before precomputing an ID; `Flow.execute` reports payload schema failures through its typed error channel.

For the progress an owner resumes, see [Durable execution](/docs/concepts/durable-execution/). For current status, see [Inspect runs](/docs/guides/inspecting-runs/).

---

# Retries and interruption
https://smithers.sh/docs/concepts/retries/

A failed action, a lost host, and a cancellation need different responses. Smithers keeps those cases separate so recovery does not restart work the caller cancelled.

| Event | Mechanism | Result |
| --- | --- | --- |
| An expected typed failure, such as a temporary upload error | Explicit `Action.retry` around the effect | Retry under the supplied options, then return the result or final error. |
| An interruption marked `InfraInterrupt` | The action's `interruptRetryPolicy` | Retry within the declared infrastructure budget. |
| User cancellation | The driver's cancellation path | Stop the run; do not consume an infrastructure retry budget. |

The [retry tutorial](/docs/tutorials/retry-policy/) demonstrates ordinary typed failures. The infrastructure policy applies to marked interruptions and has its own terminal failures.

## A retry policy is a plain value

A `RetryPolicy` is a frozen value: `initialMs`, `factor`, `maxMs`, `maxAttempts`, `expirationMs`, `jitterRatio`, and the `nonRetryable` tags. `RetryPolicy.make` validates the bounds and throws a `RangeError` naming the field that is wrong, and it copies and freezes the `nonRetryable` array so a later mutation of the caller's array cannot change what a parked policy means.

| Field          | Bound                                  |
| -------------- | -------------------------------------- |
| `initialMs`    | finite and not negative                |
| `factor`       | finite and positive                    |
| `maxMs`        | finite and not below `initialMs`       |
| `maxAttempts`  | a safe integer of at least one         |
| `expirationMs` | finite and positive                    |
| `jitterRatio`  | finite, between zero and one inclusive |

The next delay is derived from a persisted attempt count, not from fiber-local `Schedule` state. `nextDelay` mirrors Temporal's `ComputeNextDelay` and is total even for a policy decoded from a persisted row: a non-finite attempt, elapsed time, bound, or computed delay answers `None` rather than handing the engine a negative or `NaN` duration to sleep. `decide` is the engine's single retry decision point, and non-retryable classification happens there and nowhere else: by the policy's declared tags, and by `defaultNonRetryable`, the integrity verdicts that must reach the driver unretried under every policy.

Because the decision reads durable state, retry deadlines survive restarts. A policy whose `expirationMs` elapsed while the process was down is already expired when the run resumes. Inside a body, `Action.retry` is Effect retry with durable attempt context, and `CurrentAttempt` exposes the one-based durable attempt, so every attempt of one retry sequence reuses its own action's ordinals.

## The driver observes cancellation

Cancellation stops the flow fiber when the driver observes it. A cancel is a statement about the run, not an accident of the network, and retrying it would redo work the caller already refused. Durable cancellation is observed, not only recorded: while a run executes, the driver polls the run's cancel-requested flag on the heartbeat cadence and interrupts the flow fiber, and terminal transitions are guarded inside the ownership compare-and-swap, so a request that races past the last poll turns finalize into a cancellation. A cancel requested against a parked run is re-driven by the periodic sweep, so parking does not hide a run from its caller's refusal.

## Infrastructure interruptions have a separate policy

An interrupt marked `InfraInterrupt` means the host was lost or the work was rebalanced, not that the work failed. Only marked infrastructure interruptions use the action's `interruptRetryPolicy`. Ordinary typed failures use an explicit retry loop. An exhausted `interruptRetryPolicy` dies with the typed `InfraInterruptRetriesExhausted`, carrying the action, the attempts, and the final interrupt.

The action's `retryPolicy` preserves the final declared failure when its budget ends, so typed recovery, including a graph `Catch`, still runs. The action span records `retry.stopReason` as `"expired"` when the deadline passed or `"exhausted"` when the attempt budget ended, together with the one-based `retry.attempt`.

## A recorded failure replays as itself

Retry state survives the process because it is ordinary durable state. The attempt counter resumes from the persisted sequence, and a persisted `failed` attempt row replays by rethrowing the persisted domain failure, never by readmitting the step. The failure was schema-encoded before persistence, so its `_tag` survives the JSON round trip and the policy's non-retryable matching applies on replay exactly as it applied live. A crash cannot turn a settled failure back into running work, and a restart cannot reset the budget. Readmission refuses only a genuinely mid-flight row, one still marked `running`.

## Tiers bound what may be retried

Actions have an effect tier. `sealed` work is deterministic and cacheable. `compensable` work can be undone. `irreversible` work permits neither: a payment, a post. An irreversible action may be retried only with a declared idempotency key, because retrying an unkeyed irreversible effect is how a charge happens twice; attempting it fails with `IrreversibleRetryRequiresIdempotencyKey`.

Ordinal-keyed work is serialized for the same reason. `ConcurrentKeylessDispatch` refuses two live dispatches of one keyless allocation scope, because arrival order would otherwise assign the ordinals the attempts replay under.

## Choose a retry budget deliberately

Use a retry count for a bounded number of attempts and an expiration budget when elapsed time matters. A persisted infrastructure deadline can expire while the process is stopped.

`Action.retry` accepts Effect retry options except `schedule`; it maintains durable attempt context around those retries. It does not consume a `RetryPolicy` value directly. Exhausting an ordinary `Action.retry` loop returns the effect's final error; `InfraInterruptRetriesExhausted` belongs to the infrastructure interruption path.

## Related

- [Retry policy tutorial](/docs/tutorials/retry-policy/)
- [Compensation guide](/docs/guides/compensation/)
- [Durable execution](/docs/concepts/durable-execution/)
- [`@smthrs/flow` reference](/docs/reference/api/flow/)

---

# Capabilities and the host kernel
https://smithers.sh/docs/concepts/kernel/

A flow that edits files, spawns processes, and calls models holds real authority over the host it runs on. The kernel question is who granted that authority. Smithers answers with a closed list of host services, a closed vocabulary of capabilities over them, and a grant check on every call, so no flow reaches the host ambiently.

**Authorization at a host-service boundary**

An operation through a guarded host service resolves its resource and checks the run's capability ceiling and grants. Allowed operations reach the adapter; denied operations fail, and requests that need a decision wait or return PermissionRequired.

## The closed host surface

The host surface is the fixed list `HostServiceTags`, five tags long: `FileSystem`, `Path`, `ChildProcessSpawner`, `Jj`, and `HttpClient`. Each is a service a platform adapter supplies, and the kernel decorates the same tags with middleware layers. There is no second protected tag: composing a decorator over a raw host layer shadows that raw implementation for every consumer of the original tag, so a flow cannot walk past the check by importing the platform service directly. `Path` is on the list as an explicit pass-through, because path manipulation is pure and lexical and needs no check; its layer re-provides Effect's service unchanged, so every member of the closed list still carries a recorded kernel decision.

`Workspace` is not a host service. It is a separate policy input the kernel requires before it can name a filesystem capability, supplied once so the grant store and the filesystem decorator resolve resources against the same root. `CommandLine` is not one either: it is a pure renderer with no host access, turning a command into the exact string a `proc:spawn` grant names.

`HostServices.layer` composes the closed protected surface at the application boundary, in one direction: the raw platform service, then the kernel decorator consulting the grant store, then the flow-visible service.

## The capability vocabulary

`Capability.Action` is the closed vocabulary: `fs:read`, `fs:write`, `net:get`, `net:post`, `model:call`, `proc:spawn`, and the jj slots `jj:status`, `jj:diff`, `jj:snapshot`, `jj:restore`, `jj:workspace-add`, `jj:workspace-forget`, `jj:root`, and `jj:revert`. Actions are durable identity: never repurpose one, add one. A capability carries an exact resource; a `CapabilityPattern` carries a glob matched byte-exactly against the whole resource.

`CapabilitySet` supplies the ambient authority ceiling, and its public operations can only preserve or narrow authority. Capability sets are monotone: a step can declare less than its enclosing flow, never more. Rules are ordered and last-match-wins, except that an effective configured deny is a hard veto, and the default decision is `ask`.

The vocabulary is tiered with the same tiers the action model uses. `Capability.tierOf` classifies a capability as `sealed`, `compensable`, or `irreversible`: reads are sealed, undoable writes are compensable, and the rest, a `net:post`, a `proc:spawn`, an `fs:write` that escapes the workspace, are irreversible. Only the irreversible ones require an idempotency key to retry.

The pattern grammar has no escape: a resource that genuinely contains `*` or `?` cannot be granted exactly. Patterns are derived from a concrete capability with `Capability.patternFromCapability`, never built by concatenating agent-supplied text.

## Grants and the grant store

A grant is a recorded decision. `GrantStore` resolutions are `once`, `run`, `remembered`, and `deny`. An attended store suspends the asking fiber on its request until a person answers; an unattended store fails immediately with `PermissionRequired`. `JournalGrantStore` persists a decision before activating it, so a `remembered` grant survives the process, and a persistence failure leaves the decision inactive. Replay accepts only the configured producer and known event types, and a policy history past the rule ceiling fails closed. Envelope patterns are treated as a set, deduplicated and sorted under a canonical signature over plan, scope, and pattern set, so one envelope is never persisted twice.

## What a grant names

The resource is chosen so a grant cannot be read wider than it was meant. GET and HEAD requests check as `net:get` and every other method as `net:post`. For an `https:` URL the resource is the lowercased URL host; for any other scheme it is the scheme plus the host, so a grant for a host never authorizes a cleartext `http` downgrade to it. Redirects are followed above the guard, so every hop re-enters authorization independently.

A spawn checks as `proc:spawn` with the rendered command line as its resource, and the grant identity is that line alone. The working directory and environment overrides are not part of what a grant authorizes; they reach an attended surface as display metadata only. A model call inside `HttpClient.withModelCall` checks as `model:call` with the model id appended to the resource, so a grant for one model says nothing about another.

## Checked paths stay checked

A checked pathname is not handed unchanged to a path-based host. Before the grant check, the kernel resolves every existing ancestor of the path and maps a canonical path inside the workspace back to the stable logical workspace root. After any grant suspension it resolves the resource again and refuses the operation if the path now names something else, and an open handle binds its authorization to the descriptor's device and inode identity, rechecked on guarded operations. An attended grant cannot be raced by swapping the file while the person is deciding.

## Cancelling a run leaves no process behind

The containment layer rewrites each spawned command to request `SIGTERM` and then `SIGKILL` after a grace period, and records every successful spawn in a ledger that is released when its scope closes. The ledger's durable records are replayed by the next incarnation of the same host, which obtains the orphans a dead owner left and reaps one only after verifying the record names a separate process group, the owner is gone, and the record belongs to the current boot.

## Typed permission failures

The failures are part of the contract: `PermissionRequired`, `PermissionDenied`, and `GrantStoreError`, with the stable codes `permission_required` and `permission_denied`. Effect owns the `FileSystem` and `ChildProcessSpawner` error channels, so the decorator projects its failure into `PlatformError` with the structured value on `cause`, where `Permission.fromPlatformError` recovers it. `Jj` owns its interface and names `PermissionError` directly.

A permission failure is journal-safe by construction: the capability is defensively copied and the metadata snapshotted at the construction site, so a value the journal could not encode fails there, not later at the persistence boundary.

## What the boundary guarantees

Ambient authority asks nothing, which is exactly the problem: any code holding the service tag could reach the host, and nothing could say later why an operation was allowed. A grant check at the adapter call site makes authority explicit, attributable, and replayable. The journal holds the decision, the suspended fiber means the answer can take as long as a person needs, and mutable inputs are snapshotted so the operation executed after an attended grant is the operation that was authorized.

The kernel checks capabilities at adapter call sites; it does not sandbox the operating system and cannot observe host access that bypasses the decorated services. Hermetic execution additionally requires a `StepBoundary`. `GrantStore.makeNoop` is an explicit allow-all implementation for tests, not a production policy. Through those adapters, every access through the declared surface was granted, and the grant is on the record.

## Related

- [Capabilities guide](/docs/guides/capabilities/)
- [Sandboxes guide](/docs/guides/sandboxes/)
- [`@smthrs/kernel` reference](/docs/reference/api/kernel/)
- [`@smthrs/capability` reference](/docs/reference/api/capability/)

---

# Time travel
https://smithers.sh/docs/concepts/time-travel/

Time travel reads an earlier point in a run or derives work from it. Choose the operation by the result you want:

| Operation | Result | Changes persistent state? |
| --- | --- | --- |
| Inspect or replay | A view of a committed history prefix. | No. |
| Fork | A child run with its own workspace and copied history prefix. | Yes, creates child state. |
| Rewind | The same run restored to an earlier frame, with its suffix archived. | Yes, changes run and workspace state. |

**Fork preserves the parent and creates a child**

Forking at a recorded frame copies the committed prefix into a child run. The parent retains its history; the child gets a separate workspace and can be resumed independently.

## Address a frame

The library addresses a position with a run ID and a frame containing a lineage ID and sequence number. For example, this program describes an inspection through sequence 17; it needs a `TimeTravel` service and a run containing that position to execute:

```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 })
})
```

A frame is `{ lineageId, seq }`: a sequence number on one lineage of the journal. Forks create descendant lineages, so the lineage id is part of the address, and `seq: 17` names the committed prefix through entry 17 of it.

**Fork replay limitation:** copied attempt rows retain their parent digests.
Actions whose keys include the run ID execute again in the child, including
compensable and irreversible actions. An explicitly shared cache environment
can reuse eligible sealed results, but copied attempts alone do not make the
prefix replayable. Make repeated external effects idempotent before driving a
fork.

## Replay reads and never executes

`replay` folds the committed journal prefix up to the frame through a pure projection you supply, and `inspect` is the same fold under the service defaults. The fold has no dispatcher, so a replay can never re-execute a model call or a child flow. That absence is what separates replay from an engine resume. Replay is always safe: it writes nothing, it streams, and it stops reading at the frame.

## Fork drives a copy

`fork` mints and reserves a child run id, provisions a jj workspace pinned at the frame's recorded pointer, copies the journal prefix and the attempts that prefix can explain, and records the lineage edge. The child is a real run you drive from the frame; the parent is never mutated. A fork refuses `live_parent` while the parent or any ancestor is live, because a live run has no settled prefix to copy. Forking one frame twice yields two independent child runs in two workspace lanes.

## Rewind re-derives a view

`rewind` removes the journal suffix after the frame so the run's state can be re-derived from that point. It validates the position before anything durable exists, claims the run and holds the ownership lease with a heartbeat, writes an audit row, assesses every effect boundary in the suffix, compensates what can be undone, restores the jj workspace to the frame's pointer, and archives and truncates the suffix atomically. A descendant still depending on the truncated history refuses the rewind with `live_child` under the default `detachedChildren: "block"` policy. The `"cancel"` policy cancels detached children only after the truncation commits, because cancellation is terminal and has no inverse.

## Compensation undoes within declared boundaries

Effects in the truncated range are undone by the `CompensationHandlers` you register, and a handler is held to recorded evidence: 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. With no handlers provided, a crossed record that is not sealed classifies as blocking and the rewind fails `irreversible`. That is the safe default: the protocol stops rather than leave the world half reverted.

## Recovery heals a wedged run

Recovery is not an operation. Building `TimeTravel.layer` finishes or rolls back every rewind a crash interrupted, before the service accepts work. The one audit it declines is one whose run a live process still holds: that audit stays pending and a later build finishes it, so recovery never steals a run from a living owner.

## Boundaries

Time travel is not editing history. Nothing rewrites a committed entry in place: a rewind moves the suffix after the frame into an archive and records an audit row for the whole operation, so the evidence of what the run did survives its truncation. Replay is not a resume: nothing re-executes, and a coordinate the journal does not hold is a `not_found`, not a re-run. Compensation is not a general undo: it covers exactly the effects your handlers declare, within the boundaries the journal recorded. The CLI exposes these operations under `smthrs runs`; the unified MCP server exposes the corresponding command tools.

## Use the CLI

Inspect a local run at a sequence from its history:

```bash
smthrs runs inspect <run-id> --at 17
smthrs runs replay <run-id> --at 17
```

Replace `<run-id>` and `17` with the run and sequence you want to inspect. These commands read recorded state without executing actions. History commands operate on local state; they do not use a remote control-plane connection.

Forking requires an eligible parked or terminal agent run, its approved plan, and `jj`. Rewind requires an exact frame. Preview its effects before applying it:

```bash
smthrs runs rewind <run-id> --at 17 --preview
```

Only add `--yes` when you intend to archive the suffix and restore the frame. [The CLI reference](/docs/reference/cli/) describes the retained fork workspace and resuming a child.

## Failure answers

Every operation fails as a `TimeTravelError` discriminated by a closed `code`, so a caller's branch stays exhaustive: `busy` when another owner holds the run, `live_parent` and `live_child` for the ancestry refusals, `irreversible` when an effect in the truncated range cannot be undone, `rate_limited` when the supplied limiter rejects the attempt, `limit_exceeded` when the history cap is reached, and `invalid` or `not_found` for coordinates that address nothing. An error never attaches a whole effect record: a blocking assessment travels as its identity and classification, never as the effect's input or output.

## Limits and why they exist

Every read is capped by `maxHistoryEntries` (default 100,000), so one call's cost is bounded rather than a function of the journal's length; `pageSize` is a throughput knob and never changes a derived answer. The durable store is the SQLite dialect only. The engine is the producer of everything the service reads: anchors reach the snapshot tables through a projection of journal records, so the engine never writes this package's tables and the dependency arrow stays one way.

## Related

- [Durable execution](/docs/concepts/durable-execution/): the journal mechanism every time-travel verb reads.
- [Compensation](/docs/guides/compensation/): declare the rollback handlers a rewind calls.
- [Time travel tutorial](/docs/tutorials/time-travel/): walk a fork and a rewind end to end.
- [Inspecting runs](/docs/guides/inspecting-runs/): read run state without deriving from it.

---

# Sync and read-only followers
https://smithers.sh/docs/concepts/sync/

A dashboard needs to observe a run without owning its execution. `@smthrs/sync` provides that read path: a server publishes committed journal entries, and a follower applies them to its own projection.

**Apply entries before advancing the cursor**

The server sends committed entries after the follower's cursor. The follower applies a batch to its projection, then advances the cursor. If applying fails, the cursor stays behind so those entries can be delivered again.

## Catch up, then follow

`Sync.Read` returns a bounded page of history. `Sync.Subscribe` streams batches under a frame-credit limit. When that credit runs out, the follower subscribes again using its saved cursors.

A cursor belongs to one run and means “entries after this sequence.” Sequence numbers can have holes. The next event does not need to be exactly one greater than the cursor.

A workspace subscription also checks the run catalog, so newly created runs can join the stream. A lost notification can delay a read; the durable history remains the source for catching up.

## Commit your projection before its cursor

Use `SubscribeOptions.apply` to update the follower's state. The client advances its cursor only after the callback succeeds. If your database write fails, those entries can be delivered again.

Design the projection to tolerate repeated application. A transport can disconnect after a local write but before progress is acknowledged. Use recorded event identity to avoid counting the same event twice.

On reconnect, the client resumes from acknowledged cursors with bounded backoff. If compaction removed the history the follower needs, the client fails closed unless an explicit `onResync` handler restores the missing state and returns its actual run cursor at or above the compaction floor. `Sync.Snapshot` can fetch an explicitly public projection; your recovery handler must apply it before returning the cursor.

## Keep observation separate from control

Sync offers reads and subscriptions. It does not claim, resume, approve, or steer runs. A follower can write its own dashboard or audit database without acquiring execution authority over the source run.

Use the control plane for operator actions. The [control-plane guide](/docs/guides/control-plane/) covers that connection; [Sync followers](/docs/guides/sync-followers/) covers the read-only client.

## Authorize the data a follower sees

A workspace share credential limits the history a principal can read. Runs on shared branches also require their branch capability. Malformed, expired, forged, or unknown-key credentials are refused.

Subscriptions stop when their signed credential expires. Sharing history therefore requires both a valid connection and authority for the requested scope. See the [sync API reference](/docs/reference/api/sync/) for credential encoding and key rotation.

## Bound and validate incoming history

The server bounds pages by entry count and frame size. The default frame ceiling is 2 MiB. A partial page reports `done: false`; one entry larger than the frame ceiling fails with `frame_too_large`.

The client rejects frames that mix in another run, reorder sequences, or send entries at or before the cursor. Such a response produces `protocol_violation` before cursor advancement.

For retention and recovery planning, see [Back up and retain run state](/docs/guides/retention/).

---

# The agent runtime
https://smithers.sh/docs/concepts/agent-runtime/

`@smthrs/agent` runs a model-driven loop on the durable engine. A model emits a JavaScript cell, the cell calls permitted flows, and the next model request receives the results. Completed flow-call boundaries can replay after a restart.

**One agent frame**

The model emits a JavaScript cell. A QuickJS realm evaluates it, and ctx.call dispatches permitted flows through durable boundaries. Results feed the next frame until the agent finishes or reaches a limit.

## The cell loop

A frame is: model, cell, realm evaluation, durable flow calls, transition. A cell is the JavaScript program the model emits for that frame. It evaluates in a QuickJS sandbox inside one persistent realm per run, so a name bound in frame 3 is still bound in frame 9 and a cell reads what earlier cells built instead of re-deriving it. A cell's only authority is `ctx.call(flowName, input)`: there is no `ctx.fs`, no `ctx.shell`, no `ctx.mcp`. Every capability a cell reaches is an ordinary flow settling through its own keyed, journaled, permission-gated boundary. A crash or a park mid-cell re-executes the cell source from the top, replays the boundaries that already settled, and reaches the parked call deterministically.

```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",
  system: ["You are a research assistant."],
  prompt: ({ topic }) => `Research ${topic}.`
})
```

## Two adapters, one loop

`AgentSession` is the production `ControlExecutor`: a launch accepted by the control plane becomes one durable flow execution whose id is the control run id, its events go to the journal, and an operator steers and approves it. `AgentAction` declares an ordinary action (same tag, same payload schema, same durable replay) whose implementation is the loop, bounded by a declared output schema that is rendered into the run's teaching and enforced against its final answer. Neither adapter reimplements the loop. An agent that drives a foreign CLI would be another implementation of `Agent.Service`, not a second loop beside this one.

## Seats and seat resolution

A seat has two halves that live in different places on purpose. The declared half is an ordinary string: what a markdown flow's `model:` frontmatter carries and what `AgentAction`'s `seat` option takes. It holds no credentials, no endpoint, and no client. `provider:modelId` such as `anthropic:claude-sonnet-4-5` is the convention the Node CLI resolver understands, not a rule the agent enforces. The resolved half, `Seat.Seat`, is the only thing `Agent.run` accepts: a live model, the route resolver that seals its requests, and the model's context window in tokens so compaction has a real budget. `SeatResolver` is the service that resolves one into the other. The CLI installs a resolver that reads keys from the environment; a test installs one that answers with a scripted model and never touches the network. A seat the host cannot serve is a typed `Seat.SeatUnresolved`, not a run that fails halfway through.

## CellPlugin hooks

Extensibility is a plugin kernel with waterfall hooks at the points where a decision is still reversible:

- `cellRegistry` transforms the one registry used for disclosure and call resolution.
- `cellFlows` adds or transforms executable flow bindings: the array the last handler returns is both the catalog the model is shown and what the call boundary resolves against, so a plugin contributes capabilities, not descriptions of them.
- `cellModelRequest` transforms the provider-neutral request immediately before its sealed model step.

`CellPlugin.fromBindings` authors the common case, a plugin that contributes bindings, with no hook knowledge. `configResolved` is a nonfatal observer boundary: a failed observer logs one warning with its stable code and never fails the composition.

## Where the runtime sits

`@smthrs/harness` owns the engine port (`sealStep`, `call`, `record`, `suspend`, and the rest) and deliberately depends on no engine, so the browser bundle never pulls in the durable engine. `@smthrs/agent` ships `FlowEngineLike`, the port's implementation over `@smthrs/engine`, and composes the harness controller, the QuickJS sandbox, registry-backed call resolution, and the plugin kernel around it. What comes back from `Agent.run` is a framework-neutral `Stream<AgentEvent>`: a caller renders it, journals it, or ignores it.

## What the runtime owns and what the model owns

The runtime owns identity, durability, and policy. `sealStep` digests the credential-free prepared request together with declared key material into a step key; credentials are signed on after the digest and never enter it, so a replayed turn re-emits the recorded model events instead of paying the provider again, and a provider wire change re-keys the step. The runtime also owns budgets (`Sandbox.Limits` per cell, `maxFrames` per loop, `Budget` per run), quota parks, and authorization, which is checked before a boundary opens because an activity's outcome is journaled.

The model owns the cells: what to compute, which flows to call, and when to declare `ctx.done`. What it can see is the projected `ctx.flows` catalog and nothing else, and the catalog is a contract: the declaration digest a cell was written against is re-derived at the call boundary, so a declaration that moved between disclosure and dispatch is refused rather than run against a body the model never saw. The runtime never writes a cell; the model never touches the journal. The provider-tool-call loop is gone, and nothing replaced it beside the cell path.

## Related

- [First agent flow](/docs/tutorials/first-agent-flow/): run the loop end to end.
- [Model seats](/docs/guides/model-seats/): how declared seats resolve to credentialed models.
- [Agent policies](/docs/guides/agent-policies/): quotas, budgets, and correction ladders.
- [Durable execution](/docs/concepts/durable-execution/): the engine underneath the loop.

---

# Memory
https://smithers.sh/docs/concepts/memory/

A run's journal records execution history. Memory carries selected knowledge into later runs: facts, conversations, and reviewed notes. `@smthrs/memory` stores that material in SQL and supplies recall operations an agent can call.

## Choose what to retain

The store separates three record families:

| Record | Use it for | Relevant behavior |
| --- | --- | --- |
| Fact | A JSON value under a key. | Supports provenance, tags, and optional expiry. |
| Thread and messages | Conversation history. | Message identity is scoped to its thread. |
| Note | Text that needs review. | A status controls whether normal recall includes it. |

`Flows.remember` writes records. `Flows.recall` retrieves advisory material within a budget. Remembering is an irreversible write; recall is a sealed read in the declared flow model.

The [memory tutorial](/docs/tutorials/memory/) stores facts in one execution and retrieves them in another.

## Scope memory to its intended lifetime

Every record belongs to a namespace with a kind and ID. Kinds include `flow`, `agent`, `user`, and `global`. A bank is the public string form of that namespace. An unprefixed bank is flow-local; an explicit prefix selects a lifetime.

Choose the namespace according to who should share the material. A user-specific preference and a repository-wide convention should not accidentally resolve to the same bank.

`WithMemory.withMemory` attaches policy to a flow and the flows it declares:

```ts
import { Flows, WithMemory } from "@smthrs/memory"

const scopedRecall = WithMemory.withMemory(Flows.recall, {
  namespace: { kind: "flow", id: "release-notes" },
  recall: "auto",
  maxTokens: 2048,
  retain: "on-complete"
})
```

Use the annotated declaration when binding handlers so the policy reaches the implementation. A caller's explicit bank or budget overrides defaults, while policy refusals still apply: `recall: "none"` returns no rows, and `retain: "never"` suppresses retention.

:::note[The budget is measured in bytes]
Despite its name, `maxTokens` is a UTF-8 byte ceiling on the serialized result array. It is not the selected model's tokenizer count. Choose the budget with that unit in mind.
:::

## Choose a recall strategy

The store remains authoritative whichever recall implementation you provide:

| Strategy | Suitable use | Requirement or limitation |
| --- | --- | --- |
| `RecallKeyword` | Tests, browser hosts, and smaller stores. | Scans normalized text without an external service. |
| `RecallFts` | Indexed text search in SQLite. | Enable FTS for the namespace kind first; otherwise recall fails with `fts_not_enabled`. |
| `RecallSemantic` | Queries where paraphrases matter. | Requires embeddings and an opt-in projection; vectors are filtered by model. |

Keyword and FTS matching can differ because their Unicode normalization behavior differs. Semantic vectors are derived data; replacing an embedding model does not replace the stored facts.

## Maintain retained material

Maintenance runs as explicit effects that a host can schedule. `ttlGc` removes expired facts and their search projections. `limitHistory` bounds conversation history, and `compact` summarizes older material.

Losing memory changes the knowledge available to future runs. Back up the authoritative store separately from disposable search indexes. [Memory operations](/docs/guides/memory-ops/) covers policies and maintenance; the [memory reference](/docs/reference/api/memory/) specifies record identity, filters, and serialization.

---

# Why Effect
https://smithers.sh/docs/concepts/why-effect/

Smithers is a durable-execution engine built on Effect. Effect supplies the runtime primitives: the journal, the retry model, testing support, and the package boundaries all come out of what Effect gives a flow program.

## Flows are Effect programs

A flow body describes a graph of operations. Action implementations use Effect to execute work with typed failures and explicit dependencies. The engine records action outcomes at durable boundaries; arbitrary side effects outside those boundaries can run again after a restart. Errors are typed: the failure channel of a step is a union the compiler checks, and a retry policy can branch on it instead of parsing messages.

## Replace implementations with layers

An implementation attaches to a declaration as a layer, and a composition is layers merged and provided. `Greet.toLayer` supplies an action's code, `Interpreter.layer` drives a flow's body, `FlowEngine.layerMemory` or the SQLite engine store supplies the runtime, and `Layer.mergeAll` with `Layer.provideMerge` wires the stack. Swapping the engine layer moves a flow, source unchanged, from an in-memory engine to a durable one.

```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 Greet = Action.make("examples/Greet", {
  payload: { name: Schema.String },
  success: Schema.String
})

const Greeting = Flow.make("examples/Greeting", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})

const GreetingLayer = Layer.mergeAll(
  Greet.toLayer(({ name }) => Effect.succeed(`Hello, ${name}.`)),
  Interpreter.layer(Greeting)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)
```

Because behavior is context, a test provides a different context: the same flow runs against an in-memory engine, a scripted model, or a stubbed clock, with no edits to the flow source and no module mocking. This lets tests control host behavior without changing the declarations under test.

## The FlowRuntime port

`@smthrs/flow` declares `FlowRuntime`, the port the authoring APIs are written against: registration, execution, polling, interruption, resume, deferred lookup and completion, and clock scheduling. The package depends on nothing that implements it. The dependency direction is `@smthrs/flow`, then `@smthrs/engine`, then the durable stores, with no cycle and no type-only escape hatch back. That direction is what lets the whole authoring surface bundle for the browser: durability arrives with whichever runtime you provide.

## The pinned version and what it buys

Effect is pinned to exactly `4.0.0-rc.112`, and a project must resolve one Effect instance. Separate Effect instances are unsupported because their schema internals are not interoperable. The pin keeps packages on a compatible runtime; applications must still preserve their own payload and codec contracts when changing code.

On top of the pin, the engine uses three Effect guarantees everywhere. Typed errors: a step's failure channel is declared and checked, so quarantine and retry decisions branch on structure. Testable layers: any credentialed or environmental piece is swapped at composition time. Structured concurrency: a run is a scope, cancellation interrupts work when its driver observes the request, and only an interrupt explicitly marked as infrastructure consumes an infrastructure retry policy.

Effect is not an interchangeable detail. The pin is a hard requirement of the project layout, not a recommendation. [Installation](/docs/installation/) explains how the platform package's peer dependencies keep the runtime on that pin.

## Where Smithers diverges from upstream

Smithers vendors Effect's workflow surface and extends it with explicit execution IDs, content-derived step keys, and a separate policy for infrastructure interruptions. By default, an invocation without an explicit ID or declared idempotency key receives a fresh UUID. Retain that ID to resume the work, or opt into the derived source when equal inputs should identify one execution. [Execution IDs and ownership](/docs/concepts/ownership/) and [Retries and interruption](/docs/concepts/retries/) describe these contracts.

## Related

- [Installation](/docs/installation/): the pinned install line and repair commands.
- [Durable execution](/docs/concepts/durable-execution/): what the journaled program buys you.
- [Module flows](/docs/guides/module-flows/): the authoring surface these layers serve.

---

# 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 pinned install line for a library project:

```bash
pnpm add @smthrs/flow@next @smthrs/engine@next @smthrs/platform-node@next
```

The `@next` suffix is required: release candidates publish to the `next` dist-tag, and `latest` still resolves the Smithers 0.x line. Effect is pinned to exactly `4.0.0-rc.112`. For the `smthrs` command, install `npm install --global @smthrs/cli@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 with local SQLite is the only durable target. Browser coverage establishes bundling only; supplying another runtime or SQL client does not establish durable browser support.

---

# Targets
https://smithers.sh/docs/reference/targets/

Choose a reference by package or rule. Each page links to its implementation and editable source.

| Reference | Purpose |
| --- | --- |
| [Agent.Diff](/docs/reference/targets/agent-diff/) | Declares an agent that produces a bounded, gate-checked candidate diff inside a declared write-set, executed by the run verb. |
| [Filegroup](/docs/reference/targets/filegroup/) | Names a set of files under one label so other targets depend on the set instead of repeating its globs. |

---

# RC support matrix
https://smithers.sh/docs/reference/support-matrix/

This is the support reference for 1.0.0-rc.0. Sources below link to the current source declarations; release tags preserve the corresponding manifest inventory. A declared engine range is an installation requirement, not evidence that every matching version was tested.

## Runtimes

| Runtime                | RC boundary                                                                                                                                                                                                                                                                                                                      | Source of truth                                                                                                                                                                                                                                                                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Node.js                | The only supported durable runtime, with local SQLite. Release packages declare either `>=22.19.0` or `^22.19.0 \|\| >=24.11.0`; the CLI and agent packages require the narrower range. CI pins `22.19.0`; the release smoke checks both floors listed below.                                                                    | [Manifest inventory](#published-node-engine-ranges); [CI toolchains](https://github.com/smithersai/smithers/blob/main/PACKAGE.ts); [release workflow](https://github.com/smithersai/smithers/blob/main/.github/workflows/release.yml)                                                                                                            |
| Node.js release smoke  | The release workflow requires the same candidate tarballs to pass on `22.19.0` and `24.11.0`, records actual Node/npm versions, and checks installation, imports, bundling, and typechecking. Treat a candidate as verified only when both runtime receipts and its archive are retained; configured gates alone are not a pass. | [release workflow and receipt upload](https://github.com/smithersai/smithers/blob/main/.github/workflows/release.yml); [smoke implementation and version output](https://github.com/smithersai/smithers/blob/main/scripts/smoke-release.mjs)                                                                                                     |
| Bun                    | Non-durable package scope only, listed below. The adapter declares `engines.bun: ">=1.4.0"`; CI pins `1.4.1`. Durable stores refuse Bun with `unsupported_runtime`. This is not a claim that every application runs on Bun.                                                                                                      | [adapter manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/package.json); [CI toolchains](https://github.com/smithersai/smithers/blob/main/PACKAGE.ts); [NodeDatabase runtime refusal](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/src/node/NodeDatabase.ts) |
| Browsers               | bundle only. No durable flows, including when a caller supplies another SQL client.                                                                                                                                                                                                                                              | [browser bundle contract](https://github.com/smithersai/smithers/blob/main/scripts/browser-contract.mjs); [aggregate boundary](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/docs/concepts/aggregate-surface.md)                                                                                                      |
| Deno and edge runtimes | Not supported. Browser bundling does not establish Deno, Cloudflare, or Vercel engine support.                                                                                                                                                                                                                                   | [browser contract scope](https://github.com/smithersai/smithers/blob/main/scripts/browser-contract.mjs); [Node-only durable boundary](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/sqlite-only.md)                                                                                            |

### Bun package scope

These ten packages declare `bunTest` targets. The target uses Bun's package runner; the adapter README records that the resolved Vitest shim executes Node. This matrix does not establish native Bun runtime coverage. Application-specific Bun commands do not extend the package support boundary.

| Package                    | Source of truth                                                                                                        |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `@smthrs/artifacts`        | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/PACKAGE.ts)        |
| `@smthrs/canonical`        | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/PACKAGE.ts)        |
| `@smthrs/capability`       | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/PACKAGE.ts)       |
| `@smthrs/crypto`           | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/crypto/PACKAGE.ts)           |
| `@smthrs/engine`           | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/PACKAGE.ts)           |
| `@smthrs/flow`             | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/PACKAGE.ts)             |
| `@smthrs/keys`             | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/PACKAGE.ts)             |
| `@smthrs/platform-browser` | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/PACKAGE.ts) |
| `@smthrs/platform-bun`     | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/PACKAGE.ts)     |
| `@smthrs/sandbox`          | [bunTest target](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/PACKAGE.ts)          |

[BunSuite command contract](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/targets/src/BunSuite.ts); [recorded Vitest shim limitation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/README.md).

## Platforms

| Platform | RC boundary                                                                                                                                            | Source of truth                                                                                                                                                                                                                               |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Linux    | Required CI platform: `ubuntu-latest`, `advisory: false`.                                                                                              | [package matrix declaration](https://github.com/smithersai/smithers/blob/main/PACKAGE.ts); [generated CI matrix](https://github.com/smithersai/smithers/blob/main/.github/workflows/ci.yml)                                                   |
| macOS    | Development use; CI is advisory, not release-gating (`macos-latest`, `continue-on-error: true` through `matrix.advisory`). No architecture-wide claim. | [package matrix declaration](https://github.com/smithersai/smithers/blob/main/PACKAGE.ts); [generated CI matrix](https://github.com/smithersai/smithers/blob/main/.github/workflows/ci.yml)                                                   |
| Windows  | Unsupported. CI is advisory, not release-gating (`windows-latest`, `continue-on-error: true` through `matrix.advisory`).                               | [package matrix and Windows failure record](https://github.com/smithersai/smithers/blob/main/PACKAGE.ts); [POSIX host requirement](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/docs/quickstart.md) |

## Storage

| Boundary             | RC contract                                                                                                                                                                                                                                               | Source of truth                                                                                                                      |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Durable store        | Local SQLite only. PostgreSQL, PGlite, browser SQL, and other supplied SQL clients are not supported durable stores.                                                                                                                                      | [SQLite-only policy](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/docs/concepts/sqlite-only.md) |
| Schema compatibility | Existing policy: "The repository is unreleased, so each package has one authoritative initial schema rather than compatibility migrations for obsolete internal versions." This is not a promise of compatibility with earlier internal database schemas. | [journal migration policy](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/api.md#migrations)  |
| 0.x run databases    | No loading, resuming, or migration of 0.x run databases. Source migration starts new runs.                                                                                                                                                                | [Compatibility policy](/docs/migration/compatibility/); [frozen policy](https://github.com/smithersai/smithers/blob/main/README.md)  |

## Network and hosting

| Boundary                 | RC contract                                                                                                                                                                                                                                                                                          | Source of truth                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Gateway outside loopback | Requires explicit listen opt-in, bearer authentication, and TLS termination in front of the HTTP gateway. The Node gateway does not terminate TLS.                                                                                                                                                   | [network bind guide](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/guides/serve-beyond-loopback.md); [HTTP server implementation](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/src/node/NodeGateway.ts)                                                                                                                           |
| Remote watch             | `ControlClient.credential` applies to HTTP. The default WebSocket transport is unauthenticated; it does not forward that credential. Remote watch is supported only through an authenticated transport/proxy that supplies the bearer on upgrade. A browser uses the documented authenticated relay. | [ControlClient transports](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/src/ControlClient.ts); [WebSocket relay requirement](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/docs/guides/serve-beyond-loopback.md#what-the-credential-then-covers); [Worker relay authentication](/docs/reference/http-api/#worker-relay-authentication) |
| Redaction                | Recognizes credential shapes and sensitive field names. It does not track secrets through arbitrary child output; a child's direct stderr bypasses the logger.                                                                                                                                       | [redaction evidence limits](https://github.com/smithersai/smithers/blob/main/scripts/repo-contract/fault-gaps.md); [journal redaction contract](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/docs/api.md)                                                                                                                                                     |

## Known limitations and evidence

| Area               | Limit                                                                                                                                                                  | Source of truth                                                                                                                                                                                                                                |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Global run listing | Loads the run population before paging. A page limit does not bound the underlying read or memory use. No scale claim. A named run has a separate direct lookup.       | [ControlLive list path](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/src/ControlLive.ts); [SQL listRuns](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/src/SqlControlRuntime.ts)    |
| Fault tier         | Bounded cases: hosted-provider faults, timer races, and other gaps remain. Passing the required fault job does not cover every failure mode.                           | [fault coverage record](https://github.com/smithersai/smithers/blob/main/scripts/repo-contract/fault-gaps.md); [fault job](https://github.com/smithersai/smithers/blob/main/PACKAGE.ts)                                                        |
| Soak               | The streaming case covers five subscribers and 500 events. No nightly soak runner is declared for the RC; hours-long growth and hosted soak are outside this evidence. | [case 16 and omitted soak tiers](https://github.com/smithersai/smithers/blob/main/scripts/repo-contract/fault-gaps.md)                                                                                                                         |
| Production canary  | Disabled for this repository. The job is guarded on `smithersai/flows`; it is not an RC validation surface.                                                            | [canary job guard](https://github.com/smithersai/smithers/blob/main/.github/workflows/canary.yml)                                                                                                                                              |
| Provider first use | A funded first-use provider run is not part of the automated release-smoke evidence. Tarball import/typecheck success does not demonstrate a provider conversation.    | [smoke scope](https://github.com/smithersai/smithers/blob/main/scripts/smoke-release.mjs); [release checks](https://github.com/smithersai/smithers/blob/main/.github/workflows/release.yml)                                                    |
| Project migration  | A realistic migrated project is not part of the automated release-smoke evidence. Migration fixtures and source rewrites do not establish that end-to-end result.      | [smoke scope](https://github.com/smithersai/smithers/blob/main/scripts/smoke-release.mjs); [migration verification guide](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/docs/guides/set-verification-commands.md) |

## Published Node engine ranges

The release roster comes from [readWorkspaceManifests and publishedPackages](https://github.com/smithersai/smithers/blob/main/scripts/pack-release.mjs). Every value below is the literal `engines.node` field.

| Published package          | `engines.node`            | Source of truth                                                                                                    |
| -------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `@smthrs/agent`            | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/package.json)                  |
| `@smthrs/artifacts`        | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/artifacts/package.json)        |
| `@smthrs/build`            | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/package.json)                  |
| `@smthrs/build-cli`        | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/package.json)        |
| `@smthrs/canonical`        | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/canonical/package.json)        |
| `@smthrs/capability`       | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/capability/package.json)       |
| `@smthrs/cli`              | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/package.json)                        |
| `@smthrs/control`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/control/package.json)                |
| `@smthrs/core`             | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/core/package.json)             |
| `@smthrs/create-app`       | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/create-app/package.json)             |
| `@smthrs/crypto`           | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/crypto/package.json)           |
| `@smthrs/database`         | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/database/package.json)         |
| `@smthrs/engine`           | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine/package.json)           |
| `@smthrs/engine-store`     | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/engine-store/package.json)     |
| `@smthrs/errors`           | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/errors/package.json)                          |
| `@smthrs/evals`            | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/evals/package.json)            |
| `@smthrs/flow`             | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/package.json)             |
| `@smthrs/flows`            | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/package.json)                  |
| `@smthrs/gateway`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/gateway/package.json)                |
| `@smthrs/harness`          | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/harness/package.json)          |
| `@smthrs/integrations`     | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/integrations/package.json)     |
| `@smthrs/jj`               | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/jj/package.json)               |
| `@smthrs/journal`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/journal/package.json)          |
| `@smthrs/kernel`           | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/kernel/package.json)           |
| `@smthrs/keys`             | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/keys/package.json)             |
| `@smthrs/mcp`              | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/mcp/package.json)                    |
| `@smthrs/memory`           | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/memory/package.json)           |
| `@smthrs/migrate`          | `^22.19.0 \|\| >=24.11.0` | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/migrate/package.json)                |
| `@smthrs/model`            | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/model/package.json)            |
| `@smthrs/notifications`    | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/notifications/package.json)          |
| `@smthrs/observability`    | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/observability/package.json)    |
| `@smthrs/patterns`         | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/patterns/package.json)         |
| `@smthrs/plan`             | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/package.json)             |
| `@smthrs/platform-browser` | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-browser/package.json) |
| `@smthrs/platform-bun`     | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-bun/package.json)     |
| `@smthrs/platform-node`    | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/platform-node/package.json)    |
| `@smthrs/plugin`           | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/plugin/package.json)           |
| `@smthrs/registry`         | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/registry/package.json)         |
| `@smthrs/run-store`        | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/run-store/package.json)        |
| `@smthrs/sandbox`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sandbox/package.json)          |
| `@smthrs/scorers`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/scorers/package.json)          |
| `@smthrs/std`              | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/std/package.json)              |
| `@smthrs/step-cache`       | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/step-cache/package.json)       |
| `@smthrs/sync`             | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/sync/package.json)             |
| `@smthrs/targets`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/targets/package.json)          |
| `@smthrs/testing`          | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/testing/package.json)                         |
| `@smthrs/time-travel`      | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/time-travel/package.json)      |
| `@smthrs/triggers`         | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smithers/agent/triggers/package.json)         |
| `smthrs`                   | `>=22.19.0`               | [manifest](https://github.com/smithersai/smithers/blob/main/packages/smthrs-deprecation/package.json)              |

---

# smthrs flow
https://smithers.sh/docs/reference/cli/flow/

`smthrs flow` operates workflow definitions. Commands that operate existing executions live under [`smthrs runs`](/docs/reference/cli/runs/).

## list and show

`flow list` returns the discovered flow catalog. `flow show <flow>` returns one flow's identity and description. Both accept `--root`, `--remote`, and `--credential`.

```bash
smthrs flow list
smthrs flow show review
```

Replace `review` with an ID in your catalog. [Flow discovery](/docs/guides/flow-discovery/) defines how directory paths become IDs.

## plan

Compiles a plan and returns its approval payload without executing the flow. Inputs can be `key=value` arguments or a `--data` JSON object. The returned `approval` object is the payload to submit unchanged.

```text
smthrs flow plan — Compile a flow plan and its approval payload without executing it

Usage: smthrs flow plan <flow> <input...> [options]

Arguments:
  flow   Discovered flow name
  input  Input fields as key=value

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --data <string>        JSON input object

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## start

Plans, approves with run scope, and launches a flow. A local attached invocation waits for settlement or a parked state. `--detached` returns after the local child confirms durable admission; a remote launch has no local child and refuses detachment.

```text
smthrs flow start — Plan, approve, and start one flow; optionally detach after durable admission

Usage: smthrs flow start <flow> [options]

Arguments:
  flow  Discovered flow name

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --data <string>
  --detached, -d

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

A start is a launch path. To continue an existing execution, use `runs resume`.

## execute

Submits a serialized approval payload or `@file` to launch previously reviewed work. It does not grant approval on your behalf.

```text
smthrs flow execute — Execute a previously approved plan payload

Usage: smthrs flow execute <approval> [options]

Arguments:
  approval  Serialized payload or @file

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## Results and exits

An accepted launch returns an `Accepted` receipt containing `runId`. A run waiting for approval can return `Parked`. Local attached launches exit `0` for completion, `1` for failure, `3` for waiting on approval, or `130` for cancellation.

A remote invocation returns its control receipt without a local settlement wait. Inspect the run separately before treating it as completed.

Use `--format json` for scripts. [Plan, approve, and run](/docs/guides/plan-approve-run/) walks through saving the plan, submitting its approval, and launching it.

---

# smthrs runs
https://smithers.sh/docs/reference/cli/runs/

`smthrs runs` operates executions already stored in a project. Supply the run ID returned at launch or shown by `runs list`.

## Read current state

| Command | Result |
| --- | --- |
| `runs list [--flow <id>] [--status <status>]` | Matching durable runs. |
| `runs show <run>` | Current run state and an event-derived diagnosis. |
| `runs output <run> [node]` | Recorded node outputs; `result` selects the final assistant output. |
| `runs logs <run>` | Recorded events, optionally followed live. |

A successful read does not imply that the run succeeded. Read its status field. Use `--format json` for structured state and `--format jsonl` for event streams.

```text
smthrs runs logs — Read run events or follow new events as they commit

Usage: smthrs runs logs <run> [options]

Arguments:
  run  Durable run ID

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --follow
  --after <number>
  --limit <number>       Maximum events; agent history pulls default to 100

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## Control an execution

| Command | Effect |
| --- | --- |
| `runs resume <run>` | Re-drives an existing execution from persistent state. |
| `runs cancel <run>` | Requests cancellation of one run. |
| `runs cancel-all` | Cancels all nonterminal runs in the selected project. |
| `runs signal <run> <payload>` | Delivers a JSON object containing the signal name and payload. |
| `runs steer <run> --message <text>` | Sends new steering instructions. |

Use each command's `--help` or `--schema` for its exact arguments. [Steer a run](/docs/guides/steering/) explains the difference between steering, signals, and cancellation.

Current-state commands accept the connection options `--root`, `--remote`, and `--credential`. Historical operations below require the local host that owns the run state.

## Inspect history

`runs inspect <run> [--at <sequence>]` derives state and event counts at a frame. `runs replay` also reads recorded history and sealed results. Neither executes action code. Omitting `--at` selects the latest frame.

`--lineage` selects a lineage explicitly. `--limit` bounds journal entries read, defaulting to 10,000 for these CLI history commands.

## Fork or rewind

`runs fork <run> --at <sequence>` creates an isolated child from an eligible parked or terminal agent run. It requires an approved plan and `jj`. The child workspace is retained under `.flows/forks/`; use `runs resume` with the returned child ID to continue it.

Rewind archives a history suffix and restores the run and workspace to an earlier frame. It requires an exact sequence and either `--preview` or `--yes`:

```text
smthrs runs rewind — Preview or archive a run's suffix and restore an earlier frame; requires --yes to mutate

Usage: smthrs runs rewind <run> [options]

Arguments:
  run  Durable run ID

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --at <number>          Exact journal sequence to branch or rewind to
  --lineage <string>     Lineage ID; defaults to the lineage recorded at the frame
  --limit <number>       Maximum journal entries to read (default: 10000)
  --preview              Show the affected suffix and effects without changing anything
  --yes                  Confirm archiving the suffix and restoring the historical frame

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

Preview before applying a rewind. Compensation only covers declared effect boundaries, and an irreversible boundary can prevent the operation. [Time travel](/docs/concepts/time-travel/) explains the difference between inspection, fork, and rewind.

---

# smthrs approvals
https://smithers.sh/docs/reference/cli/approvals/

`smthrs approvals` reads pending requests and submits decisions. An approval is bound to the exact request payload; a run ID alone is insufficient.

## list

`smthrs approvals list` lists pending in-run requests. It does not list pre-run plans such as a trigger's waiting plan. Obtain those payloads from the plan result or the trigger's `activePlan.plan.approval` field.

## approve

Accepts a serialized payload or `@file`. The default scope is `once`; `run` extends the grant through the run, and `remembered` can apply to matching future requests.

```text
smthrs approvals approve — Approve the exact serialized payload or @file

Usage: smthrs approvals approve <approval> [options]

Arguments:
  approval

Options:
  --root <string>                Project directory; defaults to the nearest Smithers project
  --remote <string>              Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>          Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>          Path to the configured MCP servers
  --quiet                        Suppress progress messages
  --scope <once|run|remembered>   (default: once)

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

A plan-level approval records a decision without launching work. Execute the approved payload with `smthrs flow execute @approval.json`.

## deny

`smthrs approvals deny <approval>` refuses the exact request. The argument can be serialized JSON or `@file`, just as for `approve`.

All three commands accept `--root`, `--remote`, and `--credential` to select the host. Use `--format json` for a structured decision receipt.

For a parked execution, [Waiting for approval](/docs/troubleshooting/waiting-approval/) explains how to locate its request and resume after a decision.

---

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

> **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 exact `effect` peer:

```bash
pnpm add @smthrs/flow@next effect@4.0.0-rc.112
```

## 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      |

The `./*` subpath covers the top-level modules (`DurableClock`, `DurableDeferred`, `DurableQueue`, `Graph`, `HumanTask`, `Interpreter`, `Poll`, `RetryPolicy`, `Sleep`, `WaitFor`). The `exports` map maps `./internal/*`, `./*/index`, `./Action/*`, `./Flow/*`, and `./FlowRuntime/*` to `null`, so those paths do not resolve.

## Namespaces

The package index re-exports fourteen namespaces:

| Namespace         | Summary                                                              |
| ----------------- | -------------------------------------------------------------------- |
| `Action`          | Durable action definitions and combinators.                          |
| `DurableClock`    | Durable clock and timer services.                                    |
| `DurableDeferred` | Durable deferred values.                                             |
| `DurableQueue`    | Durable queues.                                                      |
| `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.                |
| `StepIdentity`    | Stable step identity construction.                                   |
| `WaitFor`         | The system wait-point action and its implementation layer.           |

## Action

`Action` exports 43 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"`.                                                                                                                                                                                                  |
| `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"`.                                                                                                                                                                                                                 |
| `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.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 later registration of one tag replaces the earlier one, and closing the registering scope restores what it replaced.

### `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, success?, error?, tier?, idempotencyKey?, nondeterministic?, 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.

### `Action.makeSystem`

- **Signature:** `makeSystem(tag: Tag, options: { payload, success?, error?, tier?, idempotencyKey?, nondeterministic?, 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 completed success or failure, under the unioned success and error schemas. One winner is persisted under `name`, so a re-driven round replays it instead of racing again.

### `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.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.

## 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. The duration must be finite and not negative.

### `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.

## 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"], ...>`
- **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.

### `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 result completes the deferred named `raceAll/<name>`, 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.

### `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>`
- **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`.

### `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"], ...>`
- **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.

### `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.

## Flow

`Flow` exports 46 symbols. Its 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`, and `boundaryMode`.                                                                                                                                                                                                                         |
| `PlacementDirective`  | Schema and type      | A schema-encodable placement directive, held as `Schema.Unknown` 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`, and `maxRounds`, and exposes `call`, `child`, `to`, `annotate`, `annotateMerge`, `execute`, `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.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.Placement`

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

The annotation key for a flow's schema-encodable placement directive. Its identifier is `"@smthrs/flow/Flow/Placement"`.

### `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, idempotencyKey?, success?, error?, suspendedRetryPolicy?, maxRounds?, annotations?, body }): Flow<Tag, PayloadSchemaOf<Payload>, Success, Error, Requires>`
- **Default:** `success` is `Schema.Void`, `error` is `Schema.Never`, `annotations` is `Context.empty()`
- **Required:** `payload` and `body`
- **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 flow with nothing to plan is an action instead. `Flow.make` throws a `RangeError` when `maxRounds` is not a positive safe integer.

### `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.done`

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

Ends a trampoline lineage with a value. The engine encodes the value with the settling flow's success schema, so a caller passes the author-facing form.

### `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<ResultEncoded<any, any>>` 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`.

### `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`, and `payload: Unknown`
- **Since:** `0.1.0`

A round that ended by handing off to the next round of its trampoline lineage.

### `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` or `resume` was given an execution id the runtime never recorded.

### `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 `awaitedDeferreds` set, and `actionState`. This package declares the contract; a runtime constructs the value. No shipped runtime reads `awaitedDeferreds`: 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.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`, and `scheduleClock`. 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.

### `FlowRuntime.WaitingAnnotation`

- **Type:** `interface WaitingAnnotation { readonly reason: string; readonly wakeAt?: number | undefined; readonly token?: string | undefined }`
- **Since:** `0.1.0`

The waiting classification a flow declares before suspending.

### `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`, and the authoring node it was observed at
- **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`.

### `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 root?: string | undefined }`
- **Default:** `root` is `"root"`
- **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`.

### `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`, `unresolved_reference`, `unsupported_call`, or `missing_operation`.

### `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.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.

### `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`.

## 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)) })` 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 }): 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 not negative, `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. `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.

### `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 retry sequence crossed the policy's `expirationMs` wall-clock bound.

### `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 retry sequence exhausted the policy's `maxAttempts` bound.

### `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.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` or by the policy's declared tag list.

### `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.

### `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.

## 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`.

## 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.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/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` or `resume` 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 retry sequence crosses the policy's `expirationMs` wall-clock bound.                                               | `code`, `actionName`, `attempt`, `expirationMs`, `lastError`            |
| `@smthrs/flow/RetryAttemptsExhausted`                  | A retry sequence exhausts 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/

> **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://engine.smithers.sh/reference/api/) 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
pnpm add @smthrs/engine@next @smthrs/flow@next
```

## 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.                                 |

### `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`.                                                                 |
| `actionRetryOrigin`     | yes      | None.                                                                                                                                      |
| `actionLatestAttempt`   | 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.                                                                                                                                      |

`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. `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.

`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`.

### `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, 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.makeInstance`

- **Signature:** `makeInstance(flow: Flow.Any, executionId: string): FlowRuntime.FlowInstance["Service"]`
- **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. 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, which is a bare execution id. A value from one space is not an address in the other.

#### `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.

#### `FlowEngine.Lineage.make`

- **Signature:** `make(runId: string, path: ReadonlyArray<string> = []): JournalLineageId`
- **Default:** `path` is `[]`.
- **Since:** `0.1.0`

Returns the lineage id of a node reached by `path` from the run root. The path only ever grows inside one run, and no engine node contributes a segment today.

### `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 lineage id every later round derives from.

#### `FlowEngine.Round.Round`

- **Type:** `interface Round { readonly lineageId: string; readonly ordinal: number }`
- **Since:** `0.1.0`

The position of one execution in its lineage: the lineage it belongs to, 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 `lineageId` must be non-empty well-formed UTF-16, and an `ordinal` must be a non-negative safe integer.

#### `FlowEngine.Round.initial`

- **Signature:** `initial(executionId: string): Round`
- **Since:** `0.1.0`

Returns the round a lineage starts at: ordinal zero, under the caller's execution id. It throws `InvalidRound` synchronously when `executionId` is empty or ill-formed UTF-16.

#### `FlowEngine.Round.executionId`

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

Derives the execution id a round runs under. The id comes from `(lineageId, 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", lineageId, 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. On an attempt above `1` with a recorded snapshot, the engine calls `restore` before taking the next one. It calls `snapshot` before every dispatch and `diff` in an ensuring finalizer after it. A compensable action dispatched with this service absent dies with `SnapshotBoundaryRequired`.

## FlowProxy

### `FlowProxy.FlowProxyCollision`

- **Type:** `class FlowProxyCollision extends Error`
- **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 Error`
- **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. `ExecutePayload<Payload>` and `ResumePayload` are internal: the first is `Schema.Struct({ payload, executionId: Schema.String })`, and the second is `Schema.Struct({ executionId: Schema.String })`.

### `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.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 lets the engine derive the id from the flow's idempotency key. 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 seven coded refusals, five 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`                                                              |
| `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 five 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_proxy_collision`, and `invalid_flow_tag`. The engine also raises three refusals defined in `@smthrs/flow`: `Flow.MaxRoundsExceeded` from `Round.next`, `Action.IrreversibleRetryRequiresIdempotencyKey` when an irreversible action retries without an idempotency key, and `Flow.ExecutionIdRequired` when an `ExecutionIdScope` returns `undefined` for a resume request.

## 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://engine.smithers.sh/reference/api/) 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/

> **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.RustToolchain.Pinned({
  channel: "1.91.0",
  components: ["clippy", "rustfmt"],
  targets: ["wasm32-unknown-unknown"]
})

// 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-5.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 }
})
```

## 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

```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"
})
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 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",
  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"
})

const biome = S.BiomeCheck({
  sources: [sources],
  deps: [],
  config: S.file("biome.json"),
  lint: true,
  format: true,
  unsafe: false
})
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]
})
```

## 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({
  bin: S.Runtime.npx("react-scan@latest"),
  args: ["http://localhost:3000"],
  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: ["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-5.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],
  secrets: [S.HttpSecret(S.Secret("GITHUB_TOKEN"), ["https://ghcr.io"])],
  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/**"]
})
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/**"]
})
const githubPr = S.Github.Pr({
  gates: [tests],
  secrets: [githubToken],
  sandbox: { network: true },
  approval: "required"
})
const pages = S.Github.Pages({
  site: build,
  secrets: [githubToken],
  sandbox: { network: true },
  approval: "required"
})
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]
})
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") })
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",
  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"
  ]
})
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` 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.

```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({ release: "22.19.0", 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 docker = S.CiToolchain.Docker({ imageStore: "containerd" })
const jj = S.CiToolchain.Jj({ release: "0.39.0" })
const rust = S.CiToolchain.Rust({
  toolchain: S.RustToolchain.Pinned({
    channel: "1.91.0",
    components: ["clippy", "rustfmt"],
    targets: ["wasm32-unknown-unknown"]
  })
})
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" }
  ]
})
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,
  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/

> **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; a `PACKAGE.ts` or `WORKSPACE.ts` file then
imports it directly:

```bash
pnpm add -D @smthrs/build-cli@next @smthrs/targets@next
```

```json
{
  "devDependencies": {
    "@smthrs/build-cli": "1.0.0-rc.0",
    "@smthrs/targets": "1.0.0-rc.0"
  }
}
```

## 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 an exact peer at `4.0.0-rc.112`.

## 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`, `owners`, and `teams` are optional. 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; 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.

### `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.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. `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.

`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.

`Shell.Build` command forms key the shell and the leading literal executable.
Declare tool dependencies for commands computed dynamically or launched later
in shell text. 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`

An agent whose accepted candidate becomes a pull request. The attrs match `Agent.Diff` except that `maxRounds` is optional and defaults to `AgentTarget.defaultPrRounds`, 3. The rule is outward, so it runs only when named explicitly, and a converged loop with no bound settle action fails with `AgentPrSettleRefused` rather than reporting a pull request that was never opened.

### `Smithers.Git`

- **Type:** the `GitTarget` module
- **Since:** `0.1.0`

The git target flavors: `Git.Commit`, `Git.Pr`, `Git.Submodules`, and `Git.Submodule`. All four run under the package executor. `Git.Submodule` and `Git.Submodules` join the `build` verb and are cacheable; `Git.Commit` and `Git.Pr` join `run` and are not.

### `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`. All seven run under the package executor and none is cacheable. `Github.Pr` is gated: an invocation whose declaration names no `HttpSecret` over `GITHUB_TOKEN`, or that declares `approval: "required"` without a granted approval, is refused with `PrRefused` before any provider call.

### `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`, `Npm.Published`, and `Npm.Downstream` are cacheable, and `Npm.Publish` is the outward one.

### `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

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.TsBuild`                | rule               | Builds a JavaScript distribution with `tsc -p`, `tsup`, or the package's own build program.                                                                                       |
| `Smithers.DtsBuild`               | rule               | Emits type declarations with `tsc --emitDeclarationOnly` or `tsup --dts-only`.                                                                                                    |
| `Smithers.Typecheck`              | rule               | Checks a package with `tsc --noEmit` or 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.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.ClaudeCliMissing`       | 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.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.                                                                                      |
| `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.RustToolchain`          | namespace and type | Declared Rust toolchains and the argv they install and run cargo with.                                                                                                            |
| `Smithers.Rust`                   | namespace and type | The workspace Rust surface, `Rust.Toolchain({ workspace, channel })` or `Rust.Toolchain({ toolchain, lockfile })`.                                                                |
| `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 publishing targets, `Changesets.Version` and `Changesets.Publish`.                                                                                      |
| `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/ClaudeCliMissing`     | The engine CLI executable is not found on the host.                                                                                                      | `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` |
| `Refused`                             | An outward invocation is refused before any outward action, with `code` `missing_secret` or `approval_unsatisfied`.                                      | `code`, `rule`                                                  |
| `PrRefused`                           | A `Github.Pr` invocation is refused before any provider call, with `code` `missing_token_secret` or `approval_unsatisfied`.                              | `code`                                                          |
| `SecretUnavailable`                   | A declared secret has no value on this host.                                                                                                             | `env`                                                           |
| `SecretAudienceDenied`                | A placeholder is used for an origin the declaration does not authorize.                                                                                  | `env`, `audience`                                               |
| `SecretValueInvalid`                  | A host value cannot safely cross an HTTP request boundary.                                                                                               | `env`                                                           |
| `WorkflowParseError`                  | A workflow file cannot be read as a GitHub Actions workflow.                                                                                             | `line`                                                          |

## 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-5.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/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 commit SHA for other events. With the default
`cancelInProgress: true`, a new PR run cancels the previous run for that PR;
each pushed commit retains its own run and verdict. Set
`cancelInProgress: false` to disable cancellation.

---

# Agent.Diff
https://smithers.sh/docs/reference/targets/agent-diff/

> **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/agent-diff.md)

`Agent.Diff` declares an agent whose accepted candidate edits are applied to the tree, confined to a declared write-set and admitted only by green gates. A minimal declaration names the prompt, the data the agent reads, the write-set, the gates, and the round bound:

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

const page = Smithers.Markdown.CodeBlocks({
  file: Smithers.file("docs/reference/wire.md"),
  lang: ["ts"]
})

const referenceDocs = Smithers.Agent.Diff({
  prompt: Smithers.file("prompts/reference.md"),
  data: [Smithers.glob("src/**/*.ts")],
  changes: ["docs/reference/wire.md"],
  gates: [page],
  maxRounds: 3
})

export const Package = Smithers.Package({ targets: { page, referenceDocs } })
```

## Attributes

The `DiffAttrs` schema declares eleven fields:

| Name        | Type                                            | Default                             | Description                                                                                                                                    |
| ----------- | ----------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent`     | `AgentSelector`                                 | the workspace agent named `default` | A workspace agent reference (`Smithers.Agents.<name>`) or an inline declaration (`Smithers.Agent.ClaudeCode`, `.Codex`, `.Pool`).              |
| `prompt`    | `Input.File`                                    | `required`                          | The prompt file. A `//` path resolves from the workspace root, any other from the declaring package. A file over 1048576 bytes fails the read. |
| `payload`   | `Record<string, Reference.InputSpec>`           | `{}`                                | Invoker inputs declared with `Smithers.Input.String`, `.Literals`, and `.Optional`, supplied as `--input name=value`.                          |
| `mcp`       | `Array<Reference.McpHttp>`                      | `[]`                                | MCP servers declared with `Smithers.Mcp.Http(name, url)`, prechecked for reachability before any session spawn.                                |
| `data`      | `Attr.Data`                                     | `required`                          | Targets and declared inputs the agent reads. `Smithers.gitDiff` entries become the review slice; the other files reach the prompt.             |
| `changes`   | `Array<string>`                                 | `required`                          | The write-set globs every candidate edit stays inside. A path outside it rejects the whole candidate.                                          |
| `gates`     | `Array<Target.Target>`                          | `required`                          | Targets that must be green against each candidate tree. An outward or Run rule as a gate refuses the invocation.                               |
| `secrets`   | `Array<Secret.HttpCredential>`                  | `optional`                          | Destination-bound credentials declared with `Smithers.HttpSecret`. The declaration records the variable name and audience, never a value.      |
| `sandbox`   | `"none" \| { network?: boolean \| "loopback" }` | `optional`                          | The confinement policy for the node. Omitted, the node keeps the workspace default confinement.                                                |
| `approval`  | `"required"`                                    | `optional`                          | Declaring it refuses the invocation before any effect, because the build system has no durable approval store.                                 |
| `maxRounds` | `number`                                        | `required`                          | Candidate and gate rounds, at least 1 and at most `maximumRounds`, which is 16.                                                                |

The declaration passes no `Target.guard`, so no attr combination is refused at construction. Three preconditions settle at plan time instead: a payload input that is missing or undeclared refuses with the field to pass as `--input`, `approval: "required"` refuses, and a gate that is an outward or Run rule refuses its consumer.

## Behavior

The implementation plans one sealed `AgentDiff` action call. `diffPayload` projects the attrs into its payload: the prompt path, the declaring package directory, the payload spec, the declared MCP servers, every `Smithers.gitDiff` declaration found in `data`, the `changes` write-set, one structural identity per gate from `AgentTarget.targetIdentity`, and `maxRounds`. Plan time reads no file and spawns no process.

Execution decodes the `--input` values against the payload spec, prechecks each declared MCP server, and expands the diff slice. A target that declares `Smithers.gitDiff` data whose expansion is empty settles green as `vacuous` with zero sessions. Otherwise the executor reads the prompt file, opens a session over the `claude` or `codex` CLI with the prompt on stdin and no tools, and renders the files the target's `data` closure names into that prompt. The verdict key covers the diff digest, the prompt and payload digest, the agent identity, the mode, and the gate identities; a green verdict replays from the store with zero spawns.

Each round the session answers with complete-file candidate edits. They apply to an overlay mechanically confined to `changes`, and the gates run against that exact candidate tree, so a gate that is red on the pre-candidate tree is what the loop exists to turn green. When every gate reports green, the executor writes the accepted edits to the working tree and the success carries the round count, the rendered candidate, the edit set, and the gate report. A red report is fed back into the next round's prompt. Exhausting `maxRounds` fails with `AgentRoundsExhausted` and preserves the final candidate and gate report as files under the cache directory. The rule acts outward, so it never serves as another target's gate.

## Channels

The declaration passes these channel schemas:

| Channel | Type                                                                                                                                                |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Success | `AgentTarget.DiffResult`, with `vacuous`, `rounds`, `diff`, `edits`, and `gateReport`                                                               |
| Error   | `AgentTarget.DiffError`, the union of `AgentNeedsInput`, `AgentMcpUnreachable`, `AgentWriteEscape`, `AgentRoundsExhausted`, and `AgentSessionError` |

## Status

The catalog row and the `Target.make` call state:

| Property         | Value     |
| ---------------- | --------- |
| Kinds            | run       |
| Cacheable        | no        |
| Declares outputs | no        |
| Route            | flow body |

## Example

A documentation writer takes a group as its data and a code-block check as its gate:

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

const cwd = "packages/wire"
const page = "docs/reference/wire.md"

/** Everything the writer may read. */
const docsSources = Smithers.Filegroup({
  srcs: [Smithers.glob("src/**/*.ts"), Smithers.file("README.md")],
  cwd
})

/** Every `ts` fence on the page compiles under strict tsc. */
const codeBlocks = Smithers.Markdown.CodeBlocks({
  file: Smithers.file(page),
  lang: ["ts"]
})

const referenceDocs = Smithers.Agent.Diff({
  agent: Smithers.Agent.ClaudeCode({ model: "opus" }),
  prompt: Smithers.file("//apps/site/prompts/reference-target-rule.md"),
  data: [docsSources],
  changes: [page],
  gates: [codeBlocks],
  maxRounds: 3
})

export const Package = Smithers.Package({
  targets: { codeBlocks, docsSources, referenceDocs }
})
```

The rule participates in the `run` verb, so the label is named explicitly:

```bash
smithers-build run //packages/wire:referenceDocs
```

## See also

- [Filegroup](/docs/reference/targets/filegroup/), the file-set rule an agent target names in `data`
- [The `@smthrs/targets` package](/docs/reference/targets/)

---

# Filegroup
https://smithers.sh/docs/reference/targets/filegroup/

> **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/filegroup.md)

A group names a set of files under one label: declared files, declared globs, and other groups, expanded as one deduplicated union. A minimal declaration names its sources and the package directory they resolve from:

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

const protos = Smithers.Filegroup({
  srcs: [Smithers.glob("proto/**/*.proto")],
  cwd: "packages/wire"
})

export const Package = Smithers.Package({ targets: { protos } })
```

## Attributes

The attrs schema declares two fields:

| Name   | Type                                               | Default    | Description                                                                                                                                             |
| ------ | -------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `srcs` | `Array<Input.File \| Input.Glob \| Target.Target>` | `required` | The files, globs, and targets the group names, in read order. A target that is not a group contributes no files.                                        |
| `cwd`  | `string`                                           | `"."`      | Package directory the declared paths and patterns resolve from. The default is the declaring `PACKAGE.ts` package; another value is workspace relative. |

## Behavior

The implementation plans one `ExpandFilegroup` action call whose payload is the flattened source list `Filegroup.sources` builds from `srcs`. The walk is depth first in `srcs` order and enters each nested group once, so a diamond of groups contributes its shared members once and the flattening is deterministic. Nested sources resolve against the nested group's own `cwd`. A target in `srcs` that is not a group is skipped by the flattening and stays an ordinary dependency edge.

The declared files and globs inside `srcs` are collected by `Target.make` as declared inputs, so the group's own key carries their digests, and nested groups are dependency edges, so their keys reach the group's key. The planner adds the files of every group reachable from a consumer to that consumer's read set, so editing any member invalidates every target that names the group. Globs expand through `Input.expandGlob`, which is package scoped and applies `.gitignore`; the expansion is deduplicated by path and sorted, and a named file that does not exist digests to null instead of failing.

`kinds` is empty, so `smithers-build build`, `test`, `lint`, and `docs` never select a group as a root. Dependency traversal, `smithers-build query`, and `smithers-build graph` ignore kinds, so a group is still addressable by label and still traversed by `deps(...)`. Under a Flow runtime, a group reached as a dependency records one `ExpandFilegroup` call that reads the named files and succeeds with them digested, which needs `ExpandFilegroupLive`. Under `smithers-build`, the group node settles green without spawning a process, and its files reach the consumer through the read set and the consumer's key.

## Channels

The declaration passes these channel schemas:

| Channel | Type                                                                                                            |
| ------- | --------------------------------------------------------------------------------------------------------------- |
| Success | `Filegroup.Files`, an array of `{ path, digest }` entries sorted by path, with a null digest for a missing file |
| Error   | `Filegroup.FilegroupError`, a tagged error with a `message` field, raised on a filesystem error                 |

## Status

The catalog row and the `Target.make` call state:

| Property         | Value                                                        |
| ---------------- | ------------------------------------------------------------ |
| Kinds            | none                                                         |
| Cacheable        | yes                                                          |
| Declares outputs | no                                                           |
| Route            | flow body                                                    |
| Executes         | Yes, through `ExpandFilegroupLive`, and only as a dependency |

## Example

A group composes with a sibling group and reaches a test target as a dependency:

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

const cwd = "packages/wire"

/** The proto sources and the schema every wire target reads. */
const wireInputs = Smithers.Filegroup({
  srcs: [Smithers.glob("proto/**/*.proto"), Smithers.file("schema.json")],
  cwd
})

/** The same set plus the code generated from it. */
const wireSources = Smithers.Filegroup({
  srcs: [wireInputs, Smithers.glob("src/generated/**/*.ts")],
  cwd
})

const test = Smithers.Vitest({
  tests: [Smithers.glob("test/**/*.test.ts")],
  sources: [Smithers.glob("src/**/*.ts")],
  deps: [wireSources],
  config: Smithers.file("vitest.config.ts"),
  environment: "node",
  passWithNoTests: false,
  cwd
})

export const Package = Smithers.Package({ targets: { test, wireInputs, wireSources } })
```

The group joins no verb, so the consumer's verb is what reaches it:

```bash
smithers-build test //packages/wire:test
```

## See also

- [Agent.Diff](/docs/reference/targets/agent-diff/), a rule that names a group in its `data`
- [The `@smthrs/targets` package](/docs/reference/targets/)

---

# @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://agent.smithers.sh)

> **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 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/).

## 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.                                                |
| `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.                                                                                    |
| `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.DeclaredText`                       | One explicitly selected memory snapshot. Omitting it injects no memory.                                                                      |
| `modelParams`        | `ModelRequest.GenerationParams`                   | Generation parameters for the run's model calls.                                                                                             |
| `modelRetryPolicy`   | `Schedule<unknown, Model.ModelFailure>`           | Overrides the bounded transport retry schedule at the model boundary.                                                                        |
| `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.defaultModelCallMs` (300,000 ms); zero disarms it.               |
| `repeatCap`          | `number`                                          | Caps consecutive repeat-observation frames. Armed by default at `CellTurn.defaultRepeatFrames`; zero disarms it.                             |
| `narrowingCap`       | `number`                                          | Caps completions bounced for narrowed evidence. Armed by default at `CellTurn.defaultNarrowingDemands`.                                      |
| `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`. |
| `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.                                                                                                    |

### 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.

## 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.

### AgentSession.Options

| 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. |
| `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.defaultModelCallMs`; 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.  |
| `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
) => { readonly eventType: string; readonly payload: unknown } | undefined
```

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.

### 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.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.

### 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, attempts, retryDelay?) => Effect<boolean, unknown>`         | Waits for the control plane to publish its `running` transition before a driver starts the engine. Fails with `LaunchFailed` when the admission budget expires at `accepted`; 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.

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` re-prompts before it reports a typed
`StructuredOutputFailure`.

### 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`        | `string`                                       | The seat id the host's `SeatResolver` resolves. An opaque string here: the resolver owns the vocabulary. Required.                                                            |
| `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.                                                                                               |
| `corrections` | `number`                                       | How many times a decode miss may be re-prompted. Falls back to `Host.defaultCorrections`, then to one. Zero declares a first miss terminal and beats a generous host default. |
| `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.                                                                                                                             |
| `maxFrames`   | `number`                                       | The cell-loop bound for this step, ahead of the host's.                                                                                                                       |

### 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.                                                                              |
| `modelRetryPolicy`   | `Schedule<unknown, Model.ModelFailure>`         | The transport retry ladder one model call runs under. Defaults to the port's own; `Schedule.recurs(0)` turns it 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. |

### 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://agent.smithers.sh/concepts/seats/).

### Seat.Seat

```ts
interface Seat {
  readonly id: string
  readonly model: Model.Model
  readonly route: FlowEngineLike.RouteResolver
  readonly contextWindowTokens: number
}
```

One resolved seat: the id it was declared as, 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.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>
}
```

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.

### 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   |

## 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://agent.smithers.sh/guides/quota-and-budgets/).

### 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.

### 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.

### 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.LatencyBudget, Budget.OnExceeded

```ts
const OnExceeded = Schema.Literals(["fail", "warn", "skip-remaining"])

interface Policy {
  readonly tokens?: TokenBudget | undefined
  readonly latency?: LatencyBudget | undefined
}

interface TokenBudget {
  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.

`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. |

### 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 record: (stepKey: string, usage: ModelEvent.Usage) => Effect.Effect<void, AccountingUnavailable>
  readonly usage: Effect.Effect<Usage, AccountingUnavailable>
  readonly usageOf: (runId: string) => Effect.Effect<Usage, 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.
- `record` accounts finite non-negative usage, idempotently in its step key,
  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.

### 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. A missing field is
not a zero budget; it is no budget at all, so an envelope that approves neither
tokens nor milliseconds produces an empty policy.

### 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 the input, output, and reasoning parts are summed.
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 }`).   |
| `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.

## 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.

### 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.

### StandardFlows.filesystem

```ts
const filesystem: (
  services: Context.Context<FileSystem.FileSystem | Path.Path>,
  search?: Search.Search
) => 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)`.

### StandardFlows.shell

```ts
const shell: (
  services: Context.Context<ChildProcessSpawner.ChildProcessSpawner | Path.Path>,
  container?: Container.Container
) => 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.

### StandardFlows.tests

```ts
const tests: (
  services: Context.Context<ChildProcessSpawner.ChildProcessSpawner | TestRunner.TestRunner>
) => FlowBinding.Source
```

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.

### StandardFlows.memory

```ts
const memory: (
  services: Context.Context<MemoryStore.MemoryStore | Recall.Recall>
) => FlowBinding.Source
```

Durable memory as two ordinary flows, `remember` and `recall` from
[`@smthrs/memory`](/docs/reference/api/memory/).

### 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))
})

const AskOutput = Schema.Struct({
  answer: Schema.String,
  approved: Schema.Boolean
})
```

### 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.

## 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.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"]),
    message: Schema.String
  }
)
```

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.
- `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
  waits by re-reading the child's run row on an interval rather than suspending
  the run, so a cell that awaits a long child holds its round open.
- `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.                                                 |

### EngineChildren.childExecutionId

```ts
const childExecutionId: (parentExecutionId: string, label: string) => string
```

The execution id a labelled child runs under, `${parentExecutionId}/child/${label}`.
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 three 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. |
| `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 three 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.modelRequest

```ts
const registry: (plugins: Plugins.Service<FlowsHooks>, initial: Registry.Registry) => ...
const flows: (plugins: Plugins.Service<FlowsHooks>, initial: ReadonlyArray<FlowBinding.Binding>) => ...
const modelRequest: (plugins: Plugins.Service<FlowsHooks>, initial: ModelRequest.ModelRequest) => ...
```

Run the three 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.

## PromoteFlows

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://agent.smithers.sh/guides/promote-flows/).

### 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, which is what makes the saved flow appear in `ctx.flows` on the
  next frame rather than the next run. 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`, `WriteFlowInput`, `WriteFlowOutput`    | `flows/write-flow`: write `flow.ts`, `flow.e2e.ts`, and the fixture under `flows/<id>/`. Input `{ id, description, flowSource, testSource, fixtureJson }`. Output `{ files }`, the root-relative paths written. Declares `writes: ["flows/**"]`. |

## 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. Every path is checked before
the first byte is written, so a rejected file cannot leave a half-saved flow on
disk. A path that reaches its file through a symbolic link is refused with
`FlowStoreError { code: "invalid_path" }` rather than followed, so a link
already in the checkout cannot redirect a save outside the root.
`PromoteFlows` writes `<root>/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.

### 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://agent.smithers.sh/concepts/engine-port/).

### 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.

### 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` | `Schedule<unknown, Model.ModelFailure>`           | Bounded model-boundary retry policy. Defaults to `defaultModelRetryPolicy`.                                                                                               |
| `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.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.

### 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 its retry events, and the
`correction` ordinal names which structured-output correction the call belonged
to.

### 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 retry policy constants

| Constant                        | Value                | What it bounds                                                                                                                                                                                              |
| ------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultModelRetryBaseMillis`   | 1,000                | The first transport retry delay.                                                                                                                                                                            |
| `defaultModelRetryFactor`       | 2                    | The factor each successive delay multiplies by.                                                                                                                                                             |
| `defaultModelRetryTimes`        | 5                    | How many times one sealed step retries.                                                                                                                                                                     |
| `defaultModelRetryWindowMillis` | 45,000               | The wall clock the ladder may span. Whichever of the count and the window arrives first ends the ladder.                                                                                                    |
| `defaultModelRetryPolicy`       | jittered exponential | The production transport retry budget, composed from the four values. Sleeps are taken on the injected clock and jitter on the injected `Random`.                                                           |
| `defaultModelOverruns`          | 1                    | How many times one sealed step re-issues a call its `modelCallMs` budget cut off, with overrun teaching prepended. An overrun costs a whole armed ceiling, so it does not share the transport retry budget. |

### 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.

## 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.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.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`.                                                                                                                            |
| `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.

## MemorySnapshotRecorder

The durable implementation of `@smthrs/memory`'s `SnapshotRecorder` port: it
translates a snapshot identity into an `EngineLike.record` boundary, 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.

---

# @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://artifacts.smithers.sh)

> **Related APIs:** [`@smthrs/crypto`](/docs/reference/api/crypto/)

`@smthrs/artifacts` exports six modules. Each is available from the root as a
namespace and 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.

## 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, options?: FileSystemOptions) => Service
declare const layerFileSystem: (
  options?: FileSystemOptions
) => Layer<ArtifactStore, never, FileSystem>
```

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.

| `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.                                                               |
| `durability`              | `"required"`       | `required` syncs the blob and its fanout directory and propagates any refusal. `best-effort` is the explicit weaker capability for a host that cannot open file handles for syncing.                                        |
| `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. Reclaiming a stale lock measures the file
and then removes it in a separate step, so once some holder has gone stale, two
processes reclaiming at the same moment can both proceed. The fences that do not
depend on this lock still stand in that case:
`ArtifactSweep.RemoveOptions.ifUnmodifiedSinceMs` and the backup lease. 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://artifacts.smithers.sh/concepts/coordination/).

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. The complete server-side contract is in
[Serve the artifact protocol](https://artifacts.smithers.sh/guides/serve-the-artifact-protocol/).

### 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:`, 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. |
| `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(FileSystem.FileSystem, (fs) => ArtifactStore.makeFileSystem(fs))`
with `RemoteArtifacts.make`.

**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.

**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.

`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://artifacts.smithers.sh/guides/reclaim-disk-space/).

## 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. See
[Fence a backup against the sweep](https://artifacts.smithers.sh/guides/fence-a-backup/).

## 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.                              |

`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.

## 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)

> **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`, writing no `node_modules`. |
| `FetchBun`  | `smithers-build/install/fetch/bun`  | The same declaration for `bun.lock`. Its layer refuses every operation.         |
| `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 }`.                                                    |
| `executeMeasure`, `executeFetch`, `executeLink`            | The Effect implementations behind the three actions.                                  |
| `MeasureLive`, `FetchPnpmLive`, `FetchBunLive`, `LinkLive` | One layer per action.                                                                 |
| `layer`                                                    | All four 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.

```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. It resolves, then refuses with `code: "unsupported"`.                                                                                              |
| `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. Keeping the layer in the schema means an
unsupported selection fails with a typed error instead of silently
approximating a verified fetch.

## 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.

```ts
import * as Runtime from "@smthrs/build/Runtime"

const layer = Runtime.layerNode({
  requirement: ">=22.19.0",
  platform: { os: "linux", arch: "x64", libc: "glibc" }
})
```

| 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 bounds a version probe runs under.                            |

The platform and the environment are options rather than reads of
`globalThis.process`, so this module never touches the host outside a service
call. A version probe selects only the executable-lookup variables out of the
environment and gives the child nothing else.

An exact pin never accepts a prerelease: `satisfies` compares
`1.3.0-canary.2` as `1.3.0` for ordering, and states the prerelease rule
separately rather than pretending the suffix was not there.

## 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)

> **Related APIs:** [`@smthrs/build`](/docs/reference/api/build/) · [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/create-app`](/docs/reference/api/create-app/) · [`@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`
maps `./*` onto `src/*.ts`, so every 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.

## The CLI

### Affected and Watch process lifetime

`@smthrs/build-cli/Affected` exports `changedPaths(root, options)`, `select`,
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`, `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.

### makeCli

```ts
const makeCli: (config?: RuntimeConfig) => Cli
```

Creates the configured `smithers-build` CLI with all fourteen commands
registered. `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 cacheUrl?: string | undefined
  readonly cacheToken?: string | undefined
  readonly signal?: AbortSignal | undefined
  readonly environment?: Readonly<Record<string, string | undefined>> | undefined
  readonly stdout?: Reporter.Terminal | undefined
  readonly stderr?: Reporter.Terminal | undefined
  readonly exit?: ((code: number) => void) | undefined
}
```

Process-scoped configuration captured before declaration evaluation.
`cacheUrl` and `cacheToken` are the already-captured remote-cache credentials.
`environment` defaults to `process.env` and is what agent-fake selection,
`PATH` lookups, and outward preconditions read. `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.

## 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.

| 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                                                                              | The tsconfig chain could not be read, or an entry lies outside the workspace.          |
| `extractSpecifiers`         | `(path: string, text: string) => ReadonlyArray<ExtractedImport>`                   | Syntax-only extraction of import, export, require, and dynamic-import sites.           |
| `ExtractedImport`           | `{ specifier: string; dynamic: boolean }`                                          | 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                                                                              | A closure computation failed.                                                          |
| `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/2"`                                                              | 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.                                    |

## 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.      |

### 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.                                              |

### 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).

## 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.                 |

## CreateApp

The `create-app` implementation. From `@smthrs/build-cli/CreateApp`.

| Export            | Signature                                               | What it is                                                              |
| ----------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
| `scaffold`        | `(options: ScaffoldOptions) => Promise<ScaffoldReport>` | Copies one template into a new directory.                               |
| `ScaffoldOptions` | `{ directory; template?; templateRoot? }`               | `template` defaults to `default`.                                       |
| `ScaffoldReport`  | `{ directory; name; template; files }`                  | What one scaffold copied.                                               |
| `templateRoot`    | `() => string`                                          | Locates the `template` directory of the installed `@smthrs/create-app`. |
| `templates`       | `(root: string) => Promise<ReadonlyArray<string>>`      | The template names a directory offers, sorted.                          |

## 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`, `agent_message_unavailable`, `empty_message`, `git_failed`, `spawn_failed`. |
| `GateFailure`      | `{ target: string; message: string }`                                                      | One red gate.                                                                                                                                                                  |
| `GateRunner`       | `{ run(gates: ReadonlyArray<Target.AnyTarget>): Promise<ReadonlyArray<GateFailure>> }`     | Runs gates against the staged 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.

## GitHooks

The `gitHooks` implementation. From `@smthrs/build-cli/GitHooks`.

| 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 `.git/hooks`.                                               |
| `install`           | `(root, rendered) => Promise<{ wrote: ReadonlyArray<string> }>`                       | Installs the rendered scripts.                                                    |
| `CheckEntry`        | `{ file: string; status: "clean" \| "stale" \| "missing" }`                           | One hook file's classification.                                                   |
| `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`.

| 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 fifteen 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.                                          |

## 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.

## 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`.                     |
| `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.                                       |

---

# @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://canonical.smithers.sh)

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.

The behavior every entry below shares is on
[The serialization contract](https://canonical.smithers.sh/serialization/).
[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html) is the normative format
specification.

## canonicalize

```ts
const canonicalize: (input: unknown) => string
```

Serializes a value into an RFC 8785 canonical JSON document.

| 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, and 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.0-rc.112` as a peer
dependency.

**Decoding** takes any value, canonicalizes it, verifies that the emitted
document parses as JSON, and returns it branded. 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.

```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 }` when the value is key material, so the rejected
input is not rendered into the schema issue. See
[Canonicalize inside an Effect pipeline](https://canonical.smithers.sh/guides/use-the-schema/).

## 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. Contains member names, never member values.          |
| `message` | `string`             | `` `${code}: ${detail} at ${path}` ``.                                                           |
| `cause`   | `unknown`            | The original error, for `canonical_tojson_threw` and `canonical_getter_threw`. Absent otherwise. |

Neither `code` nor `path` contains the rejected value, so both are safe to
report across a boundary.

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://canonical.smithers.sh/troubleshooting/).

## 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 success carries `{ ok: true, value, bytes }`; a refusal carries
`{ ok: false, code, complaint, path }`. `path` contains property names and array
indices, so callers should bound it when displaying untrusted field names.

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.

`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.

---

# @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://capability.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/)

`@smthrs/capability` exports two modules from its root entry point, and each is
also importable from `@smthrs/capability/<Module>`:

```ts
import { Capability, 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 has only the shared `effect` peer,
so both the kernel and [`@smthrs/jj`](/docs/reference/api/jj/) can 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.
:::

## 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"
  | "model:call"
  | "proc:spawn"
  | "jj:status"
  | "jj:diff"
  | "jj:snapshot"
  | "jj:restore"
  | "jj:workspace-add"
  | "jj:workspace-forget"
  | "jj:root"
  | "jj:revert"

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.

### Capability.PatternAction

```ts
type PatternAction = Action | "fs:*" | "net:*" | "model:*" | "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://capability.smithers.sh/concepts/resource-globs/).

### 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 `Error` 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 only `**` can be proven to cover anything. 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.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://capability.smithers.sh/guides/grant-a-capability-safely/).

### 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.

### 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.

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 `*` is not provable, so an envelope entry that
must prove coverage is written `**`.

### 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://capability.smithers.sh/concepts/effect-tiers/).

### 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`, `jj:status`, `jj:diff`, `jj:root`                                                       |
| `compensable`  | `jj:snapshot`, `jj:restore`, `jj:workspace-add`, `jj:workspace-forget`, `jj:revert`, and an `fs:write` inside the workspace |
| `irreversible` | `net:post`, `proc:spawn`, 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.

### 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.

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`.

[`@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://capability.smithers.sh/concepts/authorization-model/).

### 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. 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 PermissionError
```

Refines an unknown value to a kernel permission failure. It validates the whole
enumerable shape rather than the `_tag` alone, because the package ships dual
CommonJS and ESM and class identity is not stable for a dual-package consumer.
It accepts a structurally valid failure produced by another copy of the package
and rejects an excess field, a wrong-typed field, a missing `meta`, non-JSON
metadata, an unknown grant-store code, or an overlong nested capability
resource.

### 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: PermissionError) => 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, 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<PermissionError>
```

Recovers the structured failure a `toPlatformError` projection carries, so an
attended surface can still reply to the request and an unattended report can
still name the capability. It unwraps only a `PermissionDenied` reason and
validates the cause with `isPermissionError`, so a foreign platform error's
`cause` never reaches the refinement.

## Related

- [The `@smthrs/kernel` reference](/docs/reference/api/kernel/): the grant store, the
  decorating layers, and grant handling.
- [Handle a permission failure](https://capability.smithers.sh/guides/handle-a-permission-failure/): the
  procedure these three failures are built for.

---

# @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://chain.smithers.sh)

> **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`, and the terminal subset a run resolves to.                                              |
| `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`, the shared `jsonBoundary`, and `layerInProcess`.                         |
| `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.

```ts
import { Catalog, Chain, Journal, ModelAuthor, 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))
)

// `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(
  Chain.run({ goal: "fix the failing test" }).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, or
  the journal itself is not a valid chain history. 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`.
  - `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.Terminal, RunError, Services>`:
  runs a chain to a terminal outcome, resuming from whatever the journal
  already holds. 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 a `Ref`, optionally seeded with prior events.
  The seed is how tests replay and resume a chain. `append` fails with
  `journal_conflict` when `expectedPosition` does not match the current
  length.

## `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.

### 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.
- `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.

## `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.

## `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`.

### 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.

### 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.

### 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.
- `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.
- `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> }`.

### Sections

- `base: string`: the BASE section: what the agent is and what a flow is.
- `concierge: string`: the CONCIERGE section, added only for the concierge
  role.
- `rules: string`: the RULES section.
- `contract: string`: the authoring contract: what one turn'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.
- `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.
- `assemble(options): string`: assembles the full prefix in fixed order:
  BASE, CONCIERGE (concierge role only), RULES, the authoring contract, the
  catalog block.
- `forCatalog(catalog: Catalog.Service, role: Role): string`: assembles the
  prefix from a mounted catalog service: the composition that cannot diverge
  from what the chain dispatches.

## `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.

### 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.
- `make: Effect<ReadonlyArray<Catalog.Entry>, never, 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. 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: Layer.Layer<Catalog.Catalog, never, MemoryStore.MemoryStore |
  Recall.Recall>`: the memory entries 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`: the canonical digest of a
  descriptor's full declaration (name, description, capabilities, effects,
  placement, model, flows, schema references, and body reference), so
  redeclaring a flow on any of those axes changes what every call key pins.
- `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.
- `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 non-approval
parks settle as data; a child's approval park bubbles as a `CallError` whose
`cause` is `approval_required`; a failing child run dies as a defect so the
parent fails un-settled and resumes at the child's 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.
- `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://chain.smithers.sh/contract/).

---

# @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://cli.smithers.sh)

> **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/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@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.

## 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_API_KEY`, 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://cli.smithers.sh/reference/cli/) 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://cli.smithers.sh/reference/cli/#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 | `remote`, `credential`, `mcpServers`, `root`, `migrationRoot`. Read off argv and the environment before any layer is built, so a handler only ever sees a valid one. |
| `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`. |

`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`.

## 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. |
| `config` | `Effect<Application.Config, CliError.UsageError>` | `makeConfig` applied to the ambient process, with the throw converted into a typed failure. |
| `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) => Layer<WorkspaceObservation.Observer>` | The workspace observer a run's mutation accounting is measured with. |
| `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?) => EngineDurable` | The real project engine. Open, migration, and journal startup failures are promoted to defects. |
| `seatResolver`, `layerSeatResolver` | constructor, layer | Resolves a declared seat string into a credentialed model. |
| `testRunner`, `testFlows`, `checkpointStore` | constructors | The `test` flow's runner, the flows it registers, and the checkpoint store. |
| `rebuildableTransport` | constructor | The replaceable HTTP dispatcher a model captures and uses after seat resolution returns. |
| `layerExecutor` | `(registry, engine, root, environment, mcpServers?, grants?, requestExecutor?, quotaPolicy?) => Layer<ControlExecutor, never, ControlRuntime \| Journal \| NotificationQueue \| Registry>` | The production run executor. Pass the same `GrantStore` the filesystem gets. |
| `layerControl` | `(config, registry?, engine?) => 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) => 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://cli.smithers.sh/concepts/output-and-exit-codes/).

## 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. |
| `CliError` | | The union of the four. |
| `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/unstable/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 `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 `up -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` | The run id read back out of a log tail. |
| `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 \|\| >=24.11.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. |
| `digest` | `(events) => Digest` | Computes it. Missing or malformed fields become optional or zero values rather than failing. |
| `renderDiagnosis` | `(run, digest) => string` | The `smthrs status` card: verdict, activity, refusals, cause, and the exact next commands. |
| `eventLine` | `(event) => string` | One follow-mode line. |
| `renderTranscript` | `(events) => string` | The turn-by-turn transcript `smthrs 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. |

## McpServer

The stdio MCP server.

| Export | Signature | Meaning |
| --- | --- | --- |
| `protocolVersion` | `"2025-06-18"` | The MCP version advertised. |
| `maximumFrameBytes` | `4 MiB` | One request or response frame. |
| `maximumHistoryEvents`, `maximumHistoryBytes` | `10_000`, `1 MiB` | One history result. |
| `Surface` | `"raw" \| "semantic" \| "both"` | Which tool list a session sees. |
| `Envelope` | `{ ok: true, data } \| { ok: false, error: { code, message } }` | The answer every tool gives. |
| `succeeded`, `failed` | constructors | The two envelopes. |
| `Tool` | interface | `name`, `description`, `readOnly`, `schema`, `inputSchema`, `call`. |
| `supportedTools` | `ReadonlyArray<Tool>` | The eleven control-backed tools. |
| `unsupportedReasons`, `unsupportedTools` | | The twelve retired names that answer `unsupported`, and why. |
| `rawTools` | `(verbs) => ReadonlyArray<Tool>` | One directory entry per shipped verb, naming the shell command. Not a second execution path. |
| `Options`, `tools` | `{ surface?, allowedTools?, readOnly?, verbs? }` | The session's scope, and the tools it leaves. |
| `requested` | `(args) => boolean` | Whether `--mcp` was passed. |
| `optionsFromArguments` | `(args) => Options` | The session scope read from raw argv, because MCP clients configure a launch command. |
| `respond` | `(request, session, version) => ...` | Answers one request, or `undefined` for a notification. |
| `serve` | `(options) => Effect` | Serves the session on stdio. |

## Agents

The agent configurations `smthrs mcp add` writes into.

| Export | Signature | Meaning |
| --- | --- | --- |
| `serverName` | `"smithers"` | The `mcpServers` key written. |
| `Agent`, `agents` | `{ id, mcpConfig }` | `claude` at `~/.claude.json`, `codex` at `~/.codex/mcp.json`. |
| `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?) => Wired` | Registers the server, through a lock file and a temp-plus-rename with the mode preserved. |
| `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 gets a `model:` line, so the launch refuses by naming the key instead of leaving a run nothing drives. |
| `template` | `(name, seat) => string` | The scaffolded `flow.mdx`: markdown, so it needs no build step in the directory `init` just created. |
| `Scaffolded`, `scaffold` | `(root, name, environment) => Scaffolded` | Writes `flows/<name>/flow.mdx` and creates `.flows/`. An existing flow file is left exactly as it is. |

## 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.

## 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. |
| `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. |
| `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`. |
| `CodexAuth` | Locates and refreshes the Codex credential store: `refreshUrl`, `clientId`, `locate`, `parse`, `Store`, `MakeOptions`, `make`. |
| `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 journal's redaction rules 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.

## Command documentation

The per-verb reference, with arguments, flags, output, and exit codes, is
generated from the real parser and the release policy and lives on smithers.sh:
[`smthrs plan`](/docs/reference/cli/plan/), [`smthrs run`](/docs/reference/cli/run/), [`smthrs up`](/docs/reference/cli/up/),
and the rest. This page documents the library surface used to compose or embed
that executable.

---

# @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://control.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@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`.

## Control

The transport-independent control vtable. Every implementation in this package
and every client projects onto this one interface.

| 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>`                                                                                                   | 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>` | `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 \| Unauthorized>`    | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                             |
| `deny`    | same as `approve`                                                                                                                                                                                           | same as `approve`.                                                                                   |
| `steer`   | `(input: SteerInput) => Effect<Receipt, RunNotFound \| InvalidInput \| PersistenceError \| Unavailable>`                                                                                                    | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                             |
| `signal`  | `(input: SignalInput) => Effect<Receipt, RunNotFound \| NoMatchingWait \| InvalidInput \| PersistenceError \| Unavailable>`                                                                                 | `Accepted`, `AlreadyApplied`, `Conflict`, or `Terminal`.                                             |
| `cancel`  | `(input: RunMutationInput) => Effect<Receipt, RunNotFound \| ClaimLost \| InvalidInput \| PersistenceError \| Unavailable>`                                                                                 | `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.                                           |
| `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

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 }; host?: string }`. |
| `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, envelope, deployClass, executionDigest?, plan?, nodes, approval }`. `approval` is the complete payload a reviewer resubmits unchanged.      |

`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://control.smithers.sh/concepts/cancellation/).                                                                                                                                                          |
| `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 `ps` and `status` listings 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 }`.                                                                                                                                            |
| `WatchFilter`   | `{ runId?: RunId; afterSequence?: number; follow?: boolean }`. `afterSequence` requires `runId`. Omitting `follow` keeps the live stream; `false` requests a finite snapshot. |
| `ControlEvent`  | `{ 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? }`, or `{ _tag: "fires", filters?: { triggerId?, runId?, outcome? }, cursor?, limit? }`.                           |
| `ListResponse`    | `{ _tag: "flows", items, warnings?, nextCursor? }`, `{ _tag: "runs", items: RunSummary[], nextCursor? }`, `{ _tag: "triggers", items: TriggerSummary[], nextCursor? }`, or `{ _tag: "fires", items: FireSummary[], 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.                                                                                                                                                                              |

`principalId` stays on the wire and is refused by `Control.list`. Deleting the
field would move the same overbroad answer one layer out, because struct
decoding strips a property the schema does not declare and the server would
never see it.

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 }`                              |
| `AlreadyApplied` | `{ receiptId: string; runId?: RunId }`                              |
| `Parked`         | `{ receiptId: string; planId: string; status: "waiting-approval" }` |
| `Conflict`       | `{ message: string }`                                               |
| `Terminal`       | `{ runId: RunId; status: RunStatus }`                               |

### 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`                                  | The caller's claim lapsed or was fenced by a newer owner.   |
| `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.             |

| 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.

## 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)`, `installBulkGrant(token, envelope, scope)`, `resolveApproval(token, decision, principal, scope?)`, `grants` |
| Runs              | `launch(planId, digest, envelope) => Effect<LaunchResult, ...>`, `getRun(runId)`, `listRuns`, `listFlows`                                                                                           |
| Messages          | `enqueueSteer(runId, message)`, `drainSteering(runId)`, `deliverSignal(runId, signal)`, `deliveredSignals(runId)`                                                                                   |
| Resume delegation | `requestResume(runId) => Effect<number, ...>`, `pendingResumes`, `clearResume(runId, sequence)`                                                                                                     |
| Ownership         | `registerFiber(runId, fiber)`, `interrupt(runId)`, `resume(runId, options?)`, `claimFence(runId)`, `releasePending(runId, fence)`, `writeStatus(runId, fence, status)`                              |
| Identity          | `stampPrincipal(submitted?)`, `lookupMutation(key, fingerprint)`, `recordMutation(key, fingerprint, receipt)`                                                                                       |

`resume` takes `{ scope?: "launched" \| "any" }`. `"launched"` restricts the
claim to runs this plane launched, which every steer wake and every
approval-driven restart passes. An explicit `Control.resume` omits it.

`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.

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. |
| `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` and `memory/test`
  identities only. Custom identities, including bearer and agent identities,
  need explicit delegation. A principal's `kind` is not itself a role grant.

`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://control.smithers.sh/guides/approvals/#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; principal?: Omit<Principal, "stampedAt">; approvalAuthority?: ApprovalAuthority.Service }` |
| `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.                                                                                                                                                                                            |

`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://control.smithers.sh/concepts/ownership/) for the status
mapping.

## ControlExecutor

The acceptance port from the control plane into a real run executor.

| 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. |
| `layer`           | layer    | `(implementation: Service) => Layer<ControlExecutor>`                                              |
| `layerNoop`       | layer    | `(overrides?: Partial<Service>) => Layer<ControlExecutor>`                                         |

### Service

| Method                | Signature                                                          |
| --------------------- | ------------------------------------------------------------------ |
| `launch`              | `(input: Launch) => Effect<Acceptance, LaunchFailed>`              |
| `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`.                                                                                        |
| `Authenticator`       | interface | `{ authenticate: (headers: Record<string, string>) => Effect<Principal, Unauthorized> }`                                                                          |
| `BearerAuthOptions`   | interface | `{ token: string; principal: Omit<Principal, "stampedAt">; now?: () => number }`                                                                                  |
| `bearerAuthenticator` | function  | `(options: BearerAuthOptions) => Authenticator`. Constant-time comparison; missing, malformed, empty, and incorrect credentials all fail closed identically.      |
| `layerAuth`           | layer     | `(authenticator: Authenticator) => Layer<ControlAuth>`                                                                                                            |
| `layerBearerAuth`     | layer     | `(options: BearerAuthOptions) => Layer<ControlAuth>`                                                                                                              |
| `layerNoopAuth`       | layer     | `(principal?: Principal) => Layer<ControlAuth>`. Authenticates nothing.                                                                                           |

`List` and `Watch` declare the whole `ControlError` union rather than restating
its members.

## ControlServer

| 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`.                                      |

## ControlClient

| Export           | Kind       | Signature                                                                                                     |
| ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `ClientConfig`   | interface  | `{ url: string; credential?: string }`. `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`; `watch` uses the abstract WebSocket the
platform layer supplies. Declared control failures cross the wire as
themselves; everything else becomes a `TransportError` whose `retryable` flag
classifies the transport phase.

In rc.0, `ClientConfig.credential` authenticates HTTP calls only; it does not
authenticate the `watch` WebSocket upgrade. Authenticated remote watch requires
a socket implementation that sends the Authorization header, or a trusted
proxy that authenticates the caller and supplies it. The default client fails
closed against a credentialed gateway. Tokens in URL query strings are not
supported.

## Lineage

Run ancestry as the control plane reads it. See
[Run lineage](https://control.smithers.sh/concepts/lineage/).

| 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://control.smithers.sh/concepts/cancellation/).

| 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://control.smithers.sh/guides/steer-a-run/).

| 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>`                                                                                           |

## Monitor

Run health over the control plane. See
[Monitor a run and heal it](https://control.smithers.sh/guides/monitor-runs/).

| 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.

## Channels

Verified ingress. A channel verifies opaque transport data before it decodes or
maps it, and dispatches the result through `Control`.

| 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.

## 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 the measured length is checked again afterwards. Both
refusals are `InvalidInput` naming the two byte counts and no body content.

## Credential

The credential boundary. Only a `CredentialRef` crosses it. See
[Store and resolve a credential](https://control.smithers.sh/guides/store-credentials/).

| 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>`                                              |
| `rotate`  | `(reference: CredentialRef, secret: Redacted<string>) => Effect<CredentialRef, Unavailable \| Unauthorized \| CredentialConflict>` |
| `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>` |
| `layer`   | layer     | `(options: Options) => Layer<CredentialCipher, Unavailable>`          |

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, or a key that is not 32 bytes,
fails with `Unavailable` rather than a defect.

## 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://control.smithers.sh/guides/durable-storage/).

## 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://control.smithers.sh/guides/testing/).

---

# @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://core.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/)

`@smthrs/core` exports ten 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.

Every value this package constructs is inert. Nothing here executes a step,
resolves a registry name, or touches a host. `TestRuntime` is the one
deliberate exception, and it is a test helper. For the model behind these
signatures, see [Plan time](https://core.smithers.sh/concepts/plan-time/),
[Identity and key material](https://core.smithers.sh/concepts/identity/), and
[Effect envelopes](https://core.smithers.sh/concepts/effects/).

## Flow

Callable, schema-described flow declarations and their immutable combinators.
Calling a flow constructs a `FlowCall` node; it never evaluates the body.

### Flow.Flow

```ts
interface Flow<in out I extends Schema.Top, out O extends Schema.Top, out E = never> extends Pipeable {
  (input: I["Type"]): Node.Node<O["Type"], E>
  readonly input: I
  readonly output: O
  readonly name?: string | undefined
  readonly description?: string | undefined
  readonly capabilities: ReadonlyArray<string>
  readonly effects: Effects.Declaration | undefined
  readonly model?: Seat | undefined
  readonly flows?: ReadonlyArray<Reference> | undefined
  readonly prompt?: string | undefined
  readonly annotations: Context.Context<never>
  readonly body: ((input: I["Type"]) => Node.Node<O["Type"], E>) | undefined
  readonly implementation: Implementation | undefined
}
```

The input schema is invariant because it participates in both decoding and
encoding. Output schemas and errors are covariant. `model`, `flows`, and
`prompt` are advisory metadata a host reads; they also form the declaration
recorded beside the flow's implementation identity.

### Flow.Any

```ts
interface Any {
  readonly [TypeId]: object
  readonly input: Schema.Top
  readonly output: Schema.Top
}
```

The marker-only existential type, for a heterogeneous collection of flows.

### Flow.Reference

```ts
type Reference = Any | string
```

A callable flow reference accepted by a dynamic flow. Module-authored flows
pass callable flow values; markdown loaders pass unresolved registry names,
which the harness resolves before execution.

### Flow.Seat

```ts
type Seat = string & {}
```

The name of a model seat a flow may run on. A seat is referred to by name,
never by provider model id, and never by credential.

### Flow.BodyDeclaration

```ts
interface BodyDeclaration {
  readonly model?: Seat | undefined
  readonly flows?: ReadonlyArray<Reference> | undefined
  readonly prompt?: string | undefined
}
```

The seat, collaborator, and prompt declaration a body-backed flow records
beside its body digest, so a decorator that changes the declared seat changes
the flow's key material instead of disappearing from it. A flow declaring none
of the three records no declaration.

### Flow.Implementation

```ts
type Implementation =
  | {
    readonly _tag: "Body"
    readonly algorithm: "sha256-source-ephemeral/v4" | "sha256-source-captures/v4"
    readonly digest: string
    readonly declaration?: BodyDeclaration | undefined
  }
  | {
    readonly _tag: "Dynamic"
    readonly model: Seat | undefined
    readonly flows: ReadonlyArray<Reference>
    readonly prompt: string | undefined
  }
```

Implementation identity. An unannotated body receives process-local
`sha256-source-ephemeral/v4` identity because JavaScript cannot inspect closure
state; [`Node.capture`](#nodecapture) produces the cross-process-stable
`sha256-source-captures/v4` identity.

### Flow.MakeOptions

```ts
interface MakeOptions<Input extends Schema.Top, Output extends Schema.Top, E> {
  readonly name?: string | undefined
  readonly description?: string | undefined
  readonly input?: Input | undefined
  readonly output?: Output | undefined
  readonly capabilities?: ReadonlyArray<string> | undefined
  readonly effects?: Effects.Declaration | undefined
  readonly model?: Seat | undefined
  readonly flows?: ReadonlyArray<Reference> | undefined
  readonly prompt?: string | undefined
  readonly body?: ((input: Input["Type"]) => Node.Node<Output["Type"], E>) | undefined
}
```

`input` defaults to `Schema.Void` and `output` to `Schema.Unknown`.
`capabilities` is deduplicated and sorted.

### Flow.make

```ts
const make: <
  Input extends Schema.Top = typeof Schema.Void,
  Output extends Schema.Top = typeof Schema.Unknown,
  E = never
>(
  config: MakeOptions<Input, Output, E>
) => Flow<Input, Output, E>
```

Creates a callable flow. With a `body`, the flow's implementation is `Body` and
`model`, `flows`, and `prompt` form its `BodyDeclaration`. With no body but a
`model` or `flows`, the same fields form a `Dynamic` implementation and the
body defaults to one dynamic node. With none of the three, the flow is
declaration-only and throws [`FlowError`](#flowflowerror) with code
`missing_body` when called or built.

### Flow.agent

```ts
const agent: typeof make
```

An alias for `make`. An agent flow is an ordinary flow whose omitted body is
filled by its model or collaborator declaration.

### Flow.isFlow

```ts
const isFlow: (value: unknown) => value is Any
```

Returns `true` when a value is a flow.

### Flow.withCapabilities

```ts
const withCapabilities: {
  (capabilities: ReadonlyArray<string>): <I, O, E>(self: Flow<I, O, E>) => Flow<I, O, E>
  <I, O, E>(self: Flow<I, O, E>, capabilities: ReadonlyArray<string>): Flow<I, O, E>
}
```

Adds capabilities, returning a fresh flow whose capabilities are sorted and
duplicate-free.

### Flow.within

```ts
const within: {
  (placement: Placement.Placement): <I, O, E>(self: Flow<I, O, E>) => Flow<I, O, E>
  <I, O, E>(self: Flow<I, O, E>, placement: Placement.Placement): Flow<I, O, E>
}
```

Places a flow within a host directive, returning a fresh flow. The
placement-shaped special case of [`annotate`](#flowannotate).

### Flow.annotate

```ts
const annotate: {
  <I2, S>(key: Context.Key<I2, S>, value: S): <I, O, E>(self: Flow<I, O, E>) => Flow<I, O, E>
  <I, O, E, I2, S>(self: Flow<I, O, E>, key: Context.Key<I2, S>, value: S): Flow<I, O, E>
}
```

Attaches one typed annotation, returning a fresh flow. Annotations are metadata
a host or a decorator reads; they do not change the flow's implementation
digest.

### Flow.withFlows

```ts
const withFlows: {
  (flows: ReadonlyArray<Reference>): <I, O, E>(self: Flow<I, O, E>) => Flow<I, O, E>
  <I, O, E>(self: Flow<I, O, E>, flows: ReadonlyArray<Reference>): Flow<I, O, E>
}
```

Replaces the collaborators a flow declares, returning a fresh flow that keeps
its name, schemas, capabilities, effects, and annotations. For a body-backed
flow the body is untouched and the new collaborators replace the `Body`
implementation's declaration. For a body-less dynamic flow, both the default
body and the `Dynamic` implementation are rebuilt.

### Flow.withEffects

```ts
const withEffects: {
  (declaration: Effects.Declaration): <I, O, E>(self: Flow<I, O, E>) => Flow<I, O, E>
  <I, O, E>(self: Flow<I, O, E>, declaration: Effects.Declaration): Flow<I, O, E>
}
```

Replaces a flow's effect declaration, returning a fresh flow.

### Flow.sealed

```ts
const sealed: {
  (): <I, O, E>(self: Flow<I, O, E>) => Flow<I, O, E>
  <I, O, E>(self: Flow<I, O, E>): Flow<I, O, E>
}
```

Returns a fresh flow whose declaration is `hermetic` and `sealed`. A flow with
no declaration gets an empty one with those two values.

### Flow.FlowError

```ts
class FlowError extends Schema.TaggedError<FlowError>()("flows/core/FlowError", {
  code: FlowErrorCode,
  message: Schema.String
}) {}
```

Thrown by a flow call and by `Graph.build`. `FlowErrorCode` is the literal
schema of its one code, `"missing_body"`.

### 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 Flow<infer _I, infer _O, infer E> ? E : never
```

Extract a flow's decoded input type, decoded output type, and error type.

### Flow.TypeId

```ts
const TypeId: TypeId = "~flows/core/Flow"
type TypeId = "~flows/core/Flow"
```

The runtime type identifier carried by flow values.

## Node

Pipeable, pure-data nodes describing a flow graph. Constructing and combining
them records an inspectable AST.

### Node.Node

```ts
interface Node<out A, out E = never> extends Pipeable.Pipeable {
  readonly ast: Ast
}
```

`Ast` is the recorded AST type. Its shape is internal; read a plan through
[`Graph`](#graph) rather than through the AST.

### Node.Any, Node.Success, Node.Error

```ts
type Any = Node<unknown, unknown>
type Success<N> = N extends Node<infer A, infer _E> ? A : never
type Error<N> = N extends Node<infer _A, infer E> ? E : never
```

### Node.isNode

```ts
const isNode: (value: unknown) => value is Any
```

### Node.succeed

```ts
const succeed: <A>(value: A) => Node<A>
```

A node that succeeds with a constant value. The value is retained by reference
and read when `Graph.build` runs, so mutating it in between changes the
recorded identity.

### Node.fail

```ts
const fail: <E>(error: E) => Node<never, E>
```

A node that always fails with the given typed error, for re-raising inside a
recovery arm. The error enters key material, so two failures carrying different
data are two declarations. It is retained by reference, like a success value.

### Node.all

```ts
const all: <const R extends Readonly<Record<string, Any>>>(
  nodes: R
) => Node<Simplify<{ readonly [K in keyof R]: Success<R[K]> }>, Error<R[keyof R]>>
```

Combines a record of independent child nodes. A member that is not a node
raises [`NodeBuildError`](#nodenodebuilderror) with code `invalid_all_member`,
naming the member.

### Node.dynamic

```ts
function dynamic<A>(options: DynamicOptions & { readonly output?: { readonly Type: A } }): Node<A>
function dynamic(options: DynamicOptions): Node<unknown>
```

An unelaborated dynamic model node. Passing an `output` schema types the
node's success channel. `Dynamic` is the only node kind that participates in
write-conflict analysis.

### Node.DynamicOptions

```ts
interface DynamicOptions {
  readonly model?: string | undefined
  readonly flows?: ReadonlyArray<string | { readonly "~flows/core/Flow": object }> | undefined
  readonly output?: unknown
  readonly prompt?: string | undefined
  readonly effects?: Effects.Declaration | undefined
}
```

### Node.map

```ts
const map: {
  <A, B>(f: (a: A) => B): <E>(self: Node<A, E>) => Node<B, E>
  <A, E, B>(self: Node<A, E>, f: (a: A) => B): Node<B, E>
}
```

Records a deferred pure function to apply to the eventual success value.
`Graph.build` never calls it; only its identity enters the plan.

### Node.andThen

```ts
const andThen: {
  <A, B, E2>(f: (a: A) => Node<B, E2>): <E>(self: Node<A, E>) => Node<B, E | E2>
  <B, E2>(next: Node<B, E2>): <A, E>(self: Node<A, E>) => Node<B, E | E2>
  <A, E, B, E2>(self: Node<A, E>, f: (a: A) => Node<B, E2>): Node<B, E | E2>
  <A, E, B, E2>(self: Node<A, E>, next: Node<B, E2>): Node<B, E | E2>
}
```

Sequences a pure node-producing builder after a node, or a node directly when
the first success value is not needed. `Graph.build` evaluates the builder once
against a symbolic placeholder, so the downstream topology and its input
references are known before execution.

The placeholder is a name, not a value. Reading a member records an input
reference and is the intended use. Arithmetic and string interpolation coerce
it to the literal text `[planned:<path>]`, a conditional on it always takes the
truthy branch, and neither produces a diagnostic. Its `then` member is reserved
and reads as `undefined`, so the placeholder is never mistaken for a thenable.

### Node.catch

```ts
const catch: {
  <Handled, B, E2>(
    options: CatchOptions<unknown, B, E2, Handled> & { readonly error: Schema.Schema<Handled> }
  ): <A, E>(self: Node<A, E>) => Node<A | B, Exclude<E, Handled> | E2>
  <E, B, E2>(
    options: CatchOptions<E, B, E2> & { readonly error?: undefined }
  ): <A>(self: Node<A, E>) => Node<A | B, E2>
  <A, E, Handled, B, E2>(
    self: Node<A, E>,
    options: CatchOptions<E, B, E2, Handled> & { readonly error: Schema.Schema<Handled> }
  ): Node<A | B, Exclude<E, Handled> | E2>
  <A, E, B, E2>(
    self: Node<A, E>,
    options: CatchOptions<E, B, E2> & { readonly error?: undefined }
  ): Node<A | B, E2>
}
```

Recovers a node's typed failures with a statically planned arm, built once at
plan time against a symbolic error naming the protected node. With no schema
the whole typed error channel is handled; with one, the remainder stays in the
error type. The symbolic error carries the same placeholder rules as
[`andThen`](#nodeandthen).

### Node.CatchOptions

```ts
interface CatchOptions<E, B, E2, Handled = E> {
  readonly error?: Schema.Schema<Handled> | undefined
  readonly onFailure: (error: Handled) => Node<B, E2>
}
```

### Node.capture

```ts
const capture: <Args extends ReadonlyArray<unknown>, A>(
  captures: Readonly<Record<string, unknown>>,
  operation: (...args: Args) => A
) => (...args: Args) => A
```

Declares the inert values a plan-time function closes over, so its identity
folds the source text with the canonical capture data instead of a per-process
nonce. Capture data must be finite, inert, plain data; anything else raises a
`TypeError` naming the offending path. Accepted data is deeply frozen and
compared structurally, so aliasing is not identity. Capture composes: capturing
an already-captured function nests the two capture sets.

### Node.within

```ts
const within: {
  (placement: Placement.Placement): <A, E>(self: Node<A, E>) => Node<A, E>
  <A, E>(self: Node<A, E>, placement: Placement.Placement): Node<A, E>
}
```

Adds a placement annotation without changing the original node.

### Node.priority

```ts
const priority: {
  (value: number): <A, E>(self: Node<A, E>) => Node<A, E>
  <A, E>(self: Node<A, E>, value: number): Node<A, E>
}
```

Adds a scheduling priority annotation. A scheduler runs ready work with a
higher number first, and children inherit the value lexically. Priority never
enters key material. A value that is not a safe integer raises
`NodeBuildError` with code `invalid_priority`.

### Node.lane

```ts
const lane: {
  (options: Annotations.LaneOptions): <A, E>(self: Node<A, E>) => Node<A, E>
  <A, E>(self: Node<A, E>, options: Annotations.LaneOptions): Node<A, E>
}
```

Adds a worktree lane annotation. Lanes are plan-time vocabulary; no runtime in
this release executes one.

### Node.withEffects

```ts
const withEffects: {
  (declaration: Effects.Declaration): <A, E>(self: Node<A, E>) => Node<A, E>
  <A, E>(self: Node<A, E>, declaration: Effects.Declaration): Node<A, E>
}
```

Adds an effect declaration annotation. On a non-work node the declaration
narrows the envelope its children inherit and enters that container's identity;
containers are not counted a second time against their own children.

### Node.NodeBuildError

```ts
class NodeBuildError extends Schema.TaggedError<NodeBuildError>()("flows/core/NodeBuildError", {
  code: NodeBuildErrorCode,
  member: Schema.String,
  message: Schema.String
}) {}
```

`NodeBuildErrorCode` is the literal schema of its four codes:
`invalid_all_member`, `invalid_continuation`, `invalid_priority`, and
`unrepresentable_value`.

### Node.TypeId

```ts
const TypeId: TypeId
type TypeId = "~flows/core/Node"
```

## Graph

Pure graph introspection for flow declarations.

### Graph.build

```ts
const build: (flowOrNode: Flow.Any | Node.Any, input?: unknown, options?: BuildOptions) => Graph
```

Builds a graph by evaluating declared flow bodies and pure `Node.andThen`
builders exactly once against symbolic predecessor values, revealing the
complete static topology without running a node, an Effect, a `Node.map` value
transformation, or a dynamic elaboration. `input` is the flow's input and is
ignored for a node.

Values supplied to `Node.succeed`, `Node.fail`, and flow calls are retained by
reference and read here.

Throws [`Flow.FlowError`](#flowflowerror) for a body-less flow,
[`Node.NodeBuildError`](#nodenodebuilderror) for a malformed continuation, and
[`GraphBuildError`](#graphgraphbuilderror) with a limit code for an oversized
plan. Declaration problems are recorded in [`diagnostics`](#graphdiagnostics)
instead.

### Graph.BuildOptions

```ts
interface BuildOptions {
  readonly resolveLayers?: ((request: LayerRequest) => Iterable<string>) | undefined
}
```

`resolveLayers` is invoked independently for each node and must be pure. It
returns resolved host, model, and permission implementation identities as
strings, not Effect layers or runtime handles, and the result becomes the
node's `layers` key material.

### Graph.LayerRequest

```ts
interface LayerRequest {
  readonly nodeId: string
  readonly kind: NodeAst["_tag"] | "LaneMerge"
  readonly model: string | undefined
  readonly capabilities: ReadonlyArray<string>
  readonly effects: Effects.Declaration | undefined
  readonly placement: Placement.Placement | undefined
}
```

### Graph.Graph

```ts
type Graph
```

An immutable, observation-only flow graph. `build` deep-freezes everything it
constructs, so the getters hand back the graph's own values rather than copies.
Read a graph through [`nodes`](#graphnodes), [`edges`](#graphedges),
[`effects`](#grapheffects), [`placements`](#graphplacements),
[`conflicts`](#graphconflicts), [`diagnostics`](#graphdiagnostics), and
[`keyMaterial`](#graphkeymaterial); the storage fields behind those getters are
not part of the published shape.

### Graph.nodes

```ts
const nodes: (graph: Graph) => ReadonlyArray<GraphNode>
```

Returns the graph's nodes in structural preorder.

### Graph.GraphNode

```ts
interface GraphNode {
  readonly id: string
  readonly kind: NodeAst["_tag"] | "LaneMerge"
  readonly dependencies: ReadonlyArray<string>
  readonly declaredEffects: Effects.Declaration | undefined
  readonly effectiveEffects: Effects.Declaration | undefined
  readonly placement: Placement.Placement | undefined
  readonly lane: Annotations.LaneOptions | undefined
  readonly priority: number | undefined
  readonly capabilities: ReadonlyArray<string>
  readonly annotations: AnnotationsProjection
  readonly keyMaterial: KeyMaterial.KeyMaterial
}
```

`id` is the node's structural position, such as `root.andThen.all.api`. It is
traversal data and never reaches a step key. `kind` is the AST tag, or
`LaneMerge` for a merge node this package synthesized. `effectiveEffects` is
populated for work nodes only.

### Graph.AnnotationsProjection

```ts
interface AnnotationsProjection {
  readonly placement: Placement.Placement | undefined
  readonly effects: Effects.Declaration | undefined
  readonly lane: Annotations.LaneOptions | undefined
  readonly priority: number | undefined
}
```

A serializable projection of the four annotations this package resolves.

### Graph.edges

```ts
const edges: (graph: Graph) => ReadonlyArray<Edge>
```

Returns dependency edges in structural preorder.

### Graph.Edge and Graph.EdgeReason

```ts
interface Edge {
  readonly from: string
  readonly to: string
  readonly reason: EdgeReason
}

type EdgeReason = "value" | "continuation" | "conflict" | "lane-merge"
```

`value` is a structural dependency, `continuation` is a statically planned
`andThen` or `catch` arm, `conflict` is an ordering edge the write-conflict
pass added, and `lane-merge` joins two laned writers to their merge node.

### Graph.effects

```ts
const effects: (graph: Graph) => ReadonlyArray<EffectEntry>
```

Returns declared and inherited effect data for the nodes that carry either.

### Graph.EffectEntry

```ts
interface EffectEntry {
  readonly nodeId: string
  readonly declared: Effects.Declaration | undefined
  readonly effective: Effects.Declaration | undefined
}
```

### Graph.placements

```ts
const placements: (graph: Graph) => ReadonlyArray<PlacementEntry>
```

Returns resolved placement data in structural preorder, skipping nodes that
resolved none.

### Graph.PlacementEntry

```ts
interface PlacementEntry {
  readonly nodeId: string
  readonly placement: Placement.Placement
}
```

### Graph.conflicts

```ts
const conflicts: (graph: Graph) => ReadonlyArray<Conflict>
```

Returns overlapping-write conflict data.

### Graph.Conflict

```ts
interface Conflict {
  readonly nodes: readonly [string, string]
  readonly paths: ReadonlyArray<string>
  readonly strategy: "serialize" | "lane" | "fail"
  readonly mergeNodeId?: string | undefined
}
```

`strategy` is the stricter of the two declarations' `onConflict` values: `fail`
beats `lane`, and `lane` beats `serialize`. `mergeNodeId` is set only for a
`lane` conflict.

### Graph.diagnostics

```ts
const diagnostics: (graph: Graph) => ReadonlyArray<GraphBuildError>
```

Returns build diagnostics without throwing.

### Graph.GraphBuildError

```ts
class GraphBuildError extends Schema.TaggedError<GraphBuildError>()("flows/core/GraphBuildError", {
  code: GraphBuildErrorCode,
  paths: Schema.Array(Schema.String),
  nodeId: Schema.optional(Schema.String),
  nodes: Schema.optional(Schema.Tuple([Schema.String, Schema.String]))
}) {}
```

`GraphBuildErrorCode` is the literal schema of twelve codes:

| Code                       | Meaning                                                                 |
| -------------------------- | ----------------------------------------------------------------------- |
| `effect_outside_envelope`  | A step declared a path its envelope does not cover. `paths` names them. |
| `effect_mode_widening`     | A `hermetic` envelope with an `expected` step.                          |
| `effect_tier_widening`     | A step whose tier is less reversible than its envelope's.               |
| `write_conflict`           | Two work nodes overlap under `onConflict: "fail"`. `nodes` names both.  |
| `capability_outside_grant` | A called flow declares a capability the grant excludes. Advisory.       |
| `duplicate_node_id`        | Two nodes claim one structural id.                                      |
| `missing_key_material`     | A node reached `keyMaterial` without any.                               |
| `invalid_node`             | A malformed node AST. Thrown, not recorded.                             |
| `plan_too_deep`            | Nesting past `maximumGraphDepth`. Thrown.                               |
| `plan_too_large`           | A node, edge, conflict, or effect-path limit crossed. Thrown.           |
| `payload_too_deep`         | Nesting past `maximumPayloadDepth` inside one plan value. Thrown.       |
| `payload_too_large`        | Members past `maximumPayloadMembers` inside one plan value. Thrown.     |

`nodeId` is populated for the three effect codes, `missing_key_material`,
`duplicate_node_id`, `capability_outside_grant`, `invalid_node`, and the four
limit codes. For `plan_too_large` it names the node whose admission crossed the
limit. `nodes` is populated for `write_conflict`. `paths` carries the offending
value path for `payload_too_large`.

### Graph.isFatalDiagnostic

```ts
const isFatalDiagnostic: (diagnostic: GraphBuildError) => boolean
```

Reports whether a diagnostic blocks [`keyMaterial`](#graphkeymaterial). Every
code except `capability_outside_grant` is fatal. The five thrown codes are
listed as fatal so a future caller that records one cannot compile it.

### Graph.keyMaterial

```ts
const keyMaterial: (graph: Graph) => Result.Result<ReadonlyArray<KeyMaterial.Entry>, GraphBuildError>
```

Returns node-associated, digest-free key material in topological dependency
order, or fails with the first fatal diagnostic the graph carries, unchanged.
The graph-local node id is outside the material `@smthrs/plan` hashes.

### Graph limits

Every bound is exported so a test can assert on it and a generator can stay
inside it. See [Build limits](https://core.smithers.sh/concepts/limits/) for the reasoning.

| Constant                  | Value   | Bounds                                                        |
| ------------------------- | ------- | ------------------------------------------------------------- |
| `maximumGraphDepth`       | 512     | Nested node structure.                                        |
| `maximumPayloadDepth`     | 128     | Nesting inside one reflected plan value.                      |
| `maximumGraphNodes`       | 4,096   | Nodes, synthesized lane merges included.                      |
| `maximumGraphEdges`       | 65,536  | Edges, conflict and lane-merge edges included.                |
| `maximumGraphConflicts`   | 65,536  | Recorded write conflicts.                                     |
| `maximumPayloadMembers`   | 100,000 | Members one plan value expands to, summed across every level. |
| `maximumEffectPaths`      | 1,024   | Read and write paths, summed, in one declaration.             |
| `maximumPlanEffectPaths`  | 65,536  | Effect paths admitted across the plan.                        |
| `maximumEffectPathLength` | 4,096   | UTF-16 code units in one effect path.                         |
| `maximumEffectGlobs`      | 128     | Patterns, entries ending in `*`, in one read or write list.   |

## Effects

Pure effect declarations describing read and write envelopes.

### 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. `tier` says how reversible the effect is, 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.

### 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.

## 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()` when the key is absent.

### The four keys

```ts
const Placement: Context.Service<PlacementModel.Placement>
const Effects: Context.Service<EffectsModel.Declaration>
const Lane: Context.Service<LaneOptions>
const Priority: Context.Service<number>
```

These are the four keys `Graph.build` projects onto each node. `Priority` is a
signed integer ordering ready work; it is never part of step identity, so
raising it never invalidates a cached step.

### Annotations.LaneOptions

```ts
interface LaneOptions {
  readonly id: string
  readonly landing?: "merge-queue" | "manual" | undefined
}
```

## 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, defaulting to `sealed`. `body` is
the node's own declaration projected into inert data: a `FlowCall` records the
called flow's schema identity, capabilities, effects, and implementation, never
its name.

### 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 flows.
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).

### 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.lowerSkill

```ts
const lowerSkill: (text: string) => Result.Result<Flow.Flow<typeof input, typeof output, never>, MarkdownError>
```

Parses and lowers an Agent Skills document to an ordinary flow 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, never>
```

Lowers already-typed markdown metadata and a body to an ordinary flow. The
prompt is the markdown body; harnesses append non-empty runtime `args` when
rendering it. Flow names remain declarations at this layer, and no
implementation is resolved. The `smart` seat is the explicit fallback when the
frontmatter declares no `model`.

### Markdown.MarkdownFrontmatter

```ts
interface MarkdownFrontmatter {
  readonly name?: string | undefined
  readonly description?: string | undefined
  readonly model?: 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
}
```

An omitted `effects.reads` or `effects.writes` becomes empty, an omitted `mode`
becomes `hermetic`, and an omitted `onConflict` becomes `serialize`.

### 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 the `SchemaError`
from `effect/Schema` raised through `Effect.runSync`, unwrapped.

### 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-shaped derivation, so a
pure constructor can run one without a platform layer.

## TestRuntime

Pure, synchronous execution support for tests of node-building libraries. It
evaluates the deferred callbacks an in-memory node AST stores. It models no
capabilities, persistence, scheduling, retries, cache, concurrency, or
output-schema enforcement, and it is not a substitute for the durable engine.

### TestRuntime.evaluate

```ts
const evaluate: <A, E, E2 = EvaluationError>(
  node: Node.Node<A, E>,
  resolver?: Resolver<E2>
) => Result.Result<A, E | E2 | EvaluationError>
```

Evaluates a node's in-memory declaration with a deterministic leaf resolver. A
declaration nested more than 1,024 levels is refused before unbounded
recursion. With no resolver, reaching a leaf fails with code
`unresolved_node`.

### TestRuntime.evaluateInline

```ts
const evaluateInline: <A, E, E2 = EvaluationError>(
  node: Node.Node<A, E>,
  resolver?: Resolver<E2>
) => Result.Result<A, E | E2 | EvaluationError>
```

Evaluates a node while recursively entering every called flow that carries an
in-memory body. Body-less model or adapter flows still cross the resolver.

### TestRuntime.Resolver

```ts
type Resolver<E = never> = (request: Request) => Result.Result<unknown, E>

type Request = DynamicRequest | FlowCallRequest

interface DynamicRequest {
  readonly _tag: "Dynamic"
  readonly model?: string | undefined
  readonly flows: ReadonlyArray<unknown>
  readonly output?: unknown
  readonly prompt?: string | undefined
  readonly effects?: unknown
}

interface FlowCallRequest {
  readonly _tag: "FlowCall"
  readonly flow: unknown
  readonly target: unknown
  readonly input: unknown
}
```

Supplies deterministic values or typed failures for the execution leaves a pure
evaluator cannot invent. The resolver's error type flows into the result's
error channel.

### TestRuntime.EvaluationError

```ts
class EvaluationError extends Error {
  readonly code: EvaluationErrorCode
  override readonly cause: unknown
}

type EvaluationErrorCode =
  | "callback_threw"
  | "depth_exceeded"
  | "invalid_continuation"
  | "invalid_schema"
  | "missing_flow"
  | "missing_operation"
  | "resolver_threw"
  | "unresolved_node"
```

A malformed or unresolved declaration encountered by the evaluator. `cause`
carries the original thrown value where one exists. For what each code means,
see [Test a declaration without a host](https://core.smithers.sh/guides/test-a-declaration/).

---

# @smthrs/create-app
https://smithers.sh/docs/reference/api/create-app/

> **Source:** [implementation](https://github.com/smithersai/smithers/tree/main/packages/smithers/create-app/src) · [reference source](https://github.com/smithersai/smithers/blob/main/packages/smithers/create-app/docs/api.md) · [package docs](https://create-app.smithers.sh)

> **Related APIs:** [`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/harness`](/docs/reference/api/harness/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/targets`](/docs/reference/api/targets/) · [`@smthrs/testing`](/docs/reference/api/testing/)

`@smthrs/create-app` declares a Smithers app in one `PACKAGE.ts` at the app
root. Everything else is named by where it sits: pages, panes, flows, and the
three layer files a flow inherits.

Install `@smthrs/build-cli@next` and `@smthrs/targets@next`, then scaffold with
`pnpm exec smithers-build create-app my-app`. The copied manifest pins the same
RC release line. See [Installation](https://create-app.smithers.sh/installation/).

## Runtime class of each subpath

A scaffolded app splits this package across three hosts, so each subpath has a
runtime class. Import the one whose class matches the file doing the importing.

| Import                         | Runtime                |
| ------------------------------ | ---------------------- |
| `@smthrs/create-app`           | Node                   |
| `@smthrs/create-app/index`     | Node                   |
| `@smthrs/create-app/app`       | browser, workerd, Node |
| `@smthrs/create-app/ui`        | browser, workerd, Node |
| `@smthrs/create-app/runtime`   | browser, workerd, Node |
| `@smthrs/create-app/package`   | Node                   |
| `@smthrs/create-app/router`    | Node                   |
| `@smthrs/create-app/vite`      | Node                   |
| `@smthrs/create-app/testing`   | Node                   |
| `@smthrs/create-app/routesBin` | Node                   |

`routes.gen.ts` pulls `./app` and `./runtime` into the Worker bundle and
`routes.ui.gen.ts` pulls `./ui` into the browser bundle, so those three carry
no `node:` import. The rest are build and test tooling and reach the
filesystem.

The root entry point re-exports `./app` and `./package` flat, rather than as
namespaces, because it is an authoring API rather than a service API: an app
writes `defineFlow`, not `App.defineFlow`.
The explicit `@smthrs/create-app/index` subpath serves the same root module.

## @smthrs/create-app/package

### CreateApp

```ts
const CreateApp: (options: CreateAppOptions) => AppTargets
```

Declares an app. Returns the serializable manifest plus four
[`@smthrs/targets`](https://github.com/smithersai/smithers/tree/main/packages/smithers/build/targets) rules.

```ts
import { CreateApp } from "@smthrs/create-app"

export const App = CreateApp({
  name: "ledger",
  brand: { name: "Ledger", tokens: { accent: "#5288c2" } },
  deploy: { cloudflare: { workerName: "ledger", domain: "ledger.example.com" } }
})
```

`CreateAppOptions`:

| Field               | Type                      | Meaning                                               |
| ------------------- | ------------------------- | ----------------------------------------------------- |
| `name`              | `string`                  | The app's name                                        |
| `brand`             | `Brand`                   | Identity: wordmark, theme, fonts, and token overrides |
| `nav`               | `ReadonlyArray<NavGroup>` | Sidebar groups. Defaults to empty                     |
| `dirs`              | `Partial<AppDirs>`        | Source layout. Defaults to `app`, `flows`, `tools`    |
| `deploy.cloudflare` | `CloudflareDeploy`        | Worker name, custom domain, and wrangler config path  |

`AppTargets`:

| Field      | What it is                                                                                                         |
| ---------- | ------------------------------------------------------------------------------------------------------------------ |
| `manifest` | The `AppManifest` the Vite plugin serves as `virtual:smthrs-app/manifest`                                          |
| `routes`   | Regenerates the two route tables. Keyed on every file the router reads                                             |
| `dev`      | `vite` on port 5173, with the network on                                                                           |
| `build`    | `vite build`, writing `dist`                                                                                       |
| `deploy`   | `wrangler deploy`, gated on `build`, approval required, with the Cloudflare credentials declared as scoped secrets |

`CloudflareDeploy.config` defaults to `worker/wrangler.jsonc`.

## @smthrs/create-app/app

The browser-safe half: types and plain data constructors only.

### Constructors

| Export          | Signature                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------- |
| `defineAgent`   | `(options: Omit<AgentSpec, "_tag">) => AgentSpec`                                                                 |
| `defineSandbox` | `(options: Omit<SandboxSpec, "_tag">) => SandboxSpec`                                                             |
| `defineTools`   | `(options: Omit<ToolsSpec, "_tag" \| "grant"> & { grant?: ReadonlyArray<ToolsGrant> }) => ToolsSpec`              |
| `defineFlow`    | `<P extends Schema.Struct.Fields, O extends Schema.Top>(options: Omit<FlowSpec<P, O>, "_tag">) => FlowSpec<P, O>` |

`defineTools` defaults `grant` to `[{ action: "*", resource: "*" }]`, the
appliance grant. The field is required on `ToolsSpec` itself, so a spec built
by hand states its envelope rather than inheriting one silently.

### Specs

| Type             | Fields                                                                                               |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| `AgentSpec`      | `seat: string`, `system: ReadonlyArray<string>`, `limits?: { calls?: number }`, `maxFrames?: number` |
| `SandboxSpec`    | `limits: { heapBytes?: number; interruptChecks?: number; wallClockMs?: number }`                     |
| `ToolsSpec`      | `sources: ReadonlyArray<FlowBinding.Source>`, `grant: ReadonlyArray<ToolsGrant>`                     |
| `ToolsGrant`     | `action: Capability.PatternAction`, `resource: string`                                               |
| `FlowSpec<P, O>` | `description`, `payload: P`, `output: O`, `prompt: (payload) => string`, `system?`, `chat?`          |
| `AnyFlowSpec`    | `FlowSpec` with its payload type erased, which is what a route table holds                           |

`AnyFlowSpec.prompt` takes `never` rather than the erased payload: `prompt` is
contravariant in its payload, so any other erasure would refuse every concrete
flow.

`chat` is routing metadata and nothing else. Nothing in
`@smthrs/create-app/runtime` reads it.

### Brand and navigation

| Type         | Fields                                                                                                                            |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `Brand`      | `name`, `wordmark?`, `theme?: "light" \| "dark" \| "system"`, `fonts?: BrandFonts`, `tokens: Partial<Record<BrandToken, string>>` |
| `BrandFonts` | `display?`, `body?`, `mono?`, `wordmark?`, `googleFonts?: ReadonlyArray<string>`                                                  |
| `BrandToken` | The 33 token names a brand may override                                                                                           |
| `NavItem`    | `label`, `href`, `icon?`                                                                                                          |
| `NavGroup`   | `label`, `items: ReadonlyArray<NavItem>`                                                                                          |

The token names and the CSS custom properties each one sets are listed in
[Brand an app](https://create-app.smithers.sh/guides/brand-an-app/).

### Routes and the manifest

| Type               | Fields                                                   |
| ------------------ | -------------------------------------------------------- |
| `PageRoute`        | `route`, `file`                                          |
| `PaneRoute`        | `name`, `file`                                           |
| `FlowRoute`        | `id`, `file`, `agent`, `sandbox`, `tools`                |
| `AppRoutes`        | `layout: string \| undefined`, `pages`, `panes`, `flows` |
| `AppDirs`          | `app`, `flows`, `tools`                                  |
| `CloudflareDeploy` | `workerName`, `domain`, `config?`                        |
| `AppManifest`      | `name`, `brand`, `nav`, `dirs`, `deploy`                 |

### Values

| Export                | Type                         | Value                                            |
| --------------------- | ---------------------------- | ------------------------------------------------ |
| `defaultDirs`         | `AppDirs`                    | `{ app: "app", flows: "flows", tools: "tools" }` |
| `defaultCallLimit`    | `number`                     | `16`                                             |
| `defaultMaxFrames`    | `number`                     | `8`                                              |
| `routeSegmentGrammar` | `string`                     | `/^[a-z][a-z0-9-]*$/` as text                    |
| `isRouteSegment`      | `(value: string) => boolean` | Whether a value may be one route segment         |

`isRouteSegment` lives in the browser-safe half because code that predicts what
the router will accept often cannot import a filesystem walk. The `aomi`
template's promote tool runs inside a Worker and uses it to refuse a flow id
the router would refuse, before writing the file.

## @smthrs/create-app/ui

### definePane

```ts
const definePane: <P>(options: {
  props: Schema.Codec<P, unknown>
  title?: string
  fullscreen?: boolean
  render: (props: P, context: PaneContext) => ReactNode
}) => PaneDefinition<P>
```

`fullscreen` defaults to `false`. The returned definition carries both
`render`, over decoded props, and `renderUnknown`, which decodes wire props
with the schema and throws the schema's own error when they are rejected.

| Type                | What it is                                                                                            |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| `PaneContext`       | `fullscreen`, `maximize()`, `restore()`: what the shell tells a pane about its presentation           |
| `PaneDefinition<P>` | One pane with its props type                                                                          |
| `AnyPaneDefinition` | The erased half a registry holds: `title?`, `fullscreen`, `renderUnknown`                             |
| `PaneRegistry`      | `Readonly<Partial<Record<string, AnyPaneDefinition>>>`. An unrouted name is `undefined`, not an error |

### Cards and frames

| Schema          | Shape                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------- |
| `PaneCard`      | `kind: "pane"`, `id`, `name`, `title?`, `props`, `fullscreen`                            |
| `HtmlCard`      | `kind: "html"`, `id`, `title?`, `html`                                                   |
| `FlowRunCard`   | `kind: "flow-run"`, `id`, `flowId`, `executionId`, `phase`, `steps`, `result?`, `error?` |
| `FlowSavedCard` | `kind: "flow-saved"`, `id`, `flowId`, `description`, `files`                             |
| `AppCard`       | The union of the four                                                                    |
| `TurnFrame`     | One frame of a turn's NDJSON stream                                                      |

`TurnFrame` members: `delta` is assistant text, `cell` is a code cell the agent
ran, `call` is one host call with its outcome, `card` and `card.update` carry
the transcript's cards, `park` suspends the run for a human, and `done` or
`error` ends it. Each schema is also exported as a type of the same name.

## @smthrs/create-app/runtime

### materializeFlow

```ts
const materializeFlow: (id: string, spec: AnyFlowSpec, agent: AgentSpec) => MaterializedFlow
```

Binds one flow declaration to the agent layer resolved for it. Returns `id`, an
`action` named `app/<id>/agent`, and a `flow` named `app/<id>` whose body is one
call to that action. The action's system teaching is the agent layer's lines
followed by the flow's own, in that order.

### layerFor

```ts
const layerFor: (options: LayerOptions) => Layer.Layer<...>
```

The full host for one flow: the agent host, the seat resolver, the agent loop,
the sandbox and steering defaults, the action implementations, an in-memory
flow engine, and the caller's crypto. It requires no service in return, which
the package asserts at compile time through the exported type
`CompositionRootsAreComplete`.

| `LayerOptions` field | Type                         |
| -------------------- | ---------------------------- |
| `agent`              | `AgentSpec`                  |
| `sandbox`            | `SandboxSpec`                |
| `tools`              | `ToolsSpec`                  |
| `seats`              | `SeatProvider`               |
| `crypto`             | `Layer.Layer<Crypto.Crypto>` |

`SeatProvider` is one method:

```ts
interface SeatProvider {
  readonly resolve: (seatId: string) => Effect.Effect<
    { readonly model: Model.Model; readonly route: Seat.Seat["route"] },
    Seat.SeatUnresolved
  >
}
```

A resolved seat is given a 200,000 token context window. Quota refusals park
under the default policy, and the run has no spend ceiling, because this
boundary has no approved envelope from which to derive one.

### emptyRegistry

```ts
const emptyRegistry: () => Registry.Registry
```

The catalog a routed app's cells are shown: nothing. A routed app reaches its
tools through the `TOOLS.ts` binding sources, which the agent host composes
into every cell, so an empty registry is the honest declaration rather than a
placeholder.

### LayerError

```ts
class LayerError extends Error {
  readonly name: "LayerError"
  readonly code: LayerErrorCode
}
type LayerErrorCode = "invalid_grant"
```

Thrown rather than returned, because every caller wants the host build to stop.
`invalid_grant` is a `TOOLS.ts` grant whose action is not one the kernel knows,
or whose resource is longer than 4096 characters. The message names the grant's
index and the field.

## @smthrs/create-app/router

### discover

```ts
const discover: (options: RouterOptions) => AppRoutes
```

Walks an app root and returns everything the two generated files are rendered
from. `RouterOptions` is `{ root: string; dirs: AppDirs }`.

```ts
import { defaultDirs } from "@smthrs/create-app/app"
import { discover } from "@smthrs/create-app/router"

const routes = discover({ root: process.cwd(), dirs: defaultDirs })
```

### resolveLayer

```ts
const resolveLayer: (
  root: string,
  dir: string,
  kind: "AGENT.ts" | "SANDBOX.ts" | "TOOLS.ts",
  files: ReadonlySet<string>
) => string
```

The nearest file of one layer kind at `dir` or any ancestor up to and including
`root`. Both paths are normalized before the walk, and `dir` must sit inside
`root`.

### Renderers

| Export      | Signature                                                 | What it renders                             |
| ----------- | --------------------------------------------------------- | ------------------------------------------- |
| `render`    | `(routes: AppRoutes) => string`                           | `routes.gen.ts`                             |
| `renderUi`  | `(routes: AppRoutes) => string`                           | `routes.ui.gen.ts`                          |
| `renderAll` | `(routes: AppRoutes) => Readonly<Record<string, string>>` | Both, keyed by their app-root relative path |

### writeRoutes

```ts
const writeRoutes: (options: RouterOptions & { check?: boolean }) => RoutesReport
```

Discovers an app root and writes both files, or reports their drift when
`check` is set. This is the whole body of the `smithers-routes` executable and
of the Vite plugin's regeneration step, so drift checking and writing cannot
diverge.

```ts
interface RoutesReport {
  readonly files: Readonly<Record<string, RoutesFileStatus>>
  readonly stale: ReadonlyArray<string>
  readonly counts: { readonly pages: number; readonly panes: number; readonly flows: number }
}
type RoutesFileStatus = "written" | "clean" | "stale"
```

### RouterError

```ts
class RouterError extends Error {
  readonly name: "RouterError"
  readonly code: RouterErrorCode
}
type RouterErrorCode = "missing_layer" | "duplicate_name" | "invalid_name"
```

`missing_layer` is a flow with no ancestor layer file of some kind,
`duplicate_name` is two files claiming one route, and `invalid_name` is a pane,
page segment, or flow segment that is not lowercase kebab-case.

## @smthrs/create-app/vite

### createApp

```ts
const createApp: (options?: CreateAppPluginOptions) => CreateAppPlugin
```

The plugin. It regenerates both route tables when the config resolves and
whenever a routed file appears or disappears, and serves the brand and the
manifest as virtual modules.

| `CreateAppPluginOptions` field | Default                                 |
| ------------------------------ | --------------------------------------- |
| `root`                         | Vite's resolved root                    |
| `manifest`                     | `loadManifest` over `<root>/PACKAGE.ts` |
| `onRouterError`                | Report the refusal on stderr            |

`configResolved` is deliberately not routed through `onRouterError`: Vite
awaits that hook, so a refused tree at startup fails the startup.

`CreateAppPlugin` is typed by what the plugin actually uses rather than by
Vite's full hook signatures, so a host or a test can drive it directly:
`name`, `configResolved`, `configureServer`, `resolveId`, and `load`.

### The virtual modules

| Export             | Value                          | What it serves                          |
| ------------------ | ------------------------------ | --------------------------------------- |
| `brandModuleId`    | `virtual:smthrs-app/brand.css` | The brand as CSS custom properties      |
| `manifestModuleId` | `virtual:smthrs-app/manifest`  | The `AppManifest` as the default export |

### brandCss

```ts
const brandCss: (brand: Brand) => string
```

Renders a brand as one CSS rule of custom properties, scoped to
`:root, [data-theme]`. A token the brand did not declare is not emitted, so the
styleguide default survives. Google Fonts `@import` rules come first, because
CSS ignores an `@import` that follows a rule.

### loadManifest

```ts
const loadManifest: (root: string) => Promise<AppManifest>
```

Loads an app's manifest by evaluating its `PACKAGE.ts` through `tsx`. The
config process is not a TypeScript process and `PACKAGE.ts` imports
`@smthrs/targets`, so the manifest cannot simply be imported. `tsx` is an
optional peer for exactly this reason: an app that passes
`CreateAppPluginOptions.manifest` never needs it.

## @smthrs/create-app/testing

### cachedModelTest

```ts
const cachedModelTest: <P, O>(name: string, options: CachedModelTestOptions<P, O>) => void
```

Registers one Vitest test that runs a routed flow on a cached model.

```ts
cachedModelTest("chat answers a balance question", {
  fixture: new URL("./fixtures/balance.json", import.meta.url),
  flow: "chat",
  payload: { message: "What is vitalik.eth's balance?" },
  expect: (output) => {
    expect(output.answer).toContain("ETH")
  }
})
```

| `CachedModelTestOptions` field | Default                       | Meaning                                                    |
| ------------------------------ | ----------------------------- | ---------------------------------------------------------- |
| `fixture`                      | required                      | `URL` of the recorded transcript                           |
| `flow`                         | required                      | The routed flow id                                         |
| `payload`                      | required                      | The flow's payload                                         |
| `expect`                       | required                      | Assertions over the decoded output                         |
| `live`                         | none                          | Builds the live model used when `SMTHRS_RECORD=1`          |
| `routes`                       | re-run the router over `root` | Loads the routed flows this test may run                   |
| `dirs`                         | `defaultDirs`                 | Source directories, when the app does not use the defaults |
| `root`                         | `process.cwd()`               | The app root the default loader walks                      |

Replay is the default: the fixture is decoded with
[`@smthrs/testing`](/docs/reference/api/testing/)'s `Fixture` schema and served by
`RecordedModel`, with no network and no API key. `SMTHRS_RECORD=1` builds the
live model from `live`, captures every request and event, and rewrites the
fixture after a run that reached its assertions. A recording that failed leaves
the committed fixture untouched, and the write goes through a temporary file
and a rename, so an interrupted process cannot truncate it.

A recorded provider refusal is stored whole, retry metadata included, and
reconstructed field for field on replay. A kernel permission decision is not
recorded: it is not a provider response, and replaying it would hand the code
under test a refusal the provider never made.

The default loader re-runs the router and imports only the named flow and its
three layer files. `routes.gen.ts` is deliberately not used: it statically
imports every page and the shell layout, which pull in React and a virtual
module that exists only while Vite is running.

### The rest

| Export               | Signature                                                                      | What it is                                                                        |
| -------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `runCachedModelTest` | `<P, O>(name: string, options: CachedModelTestOptions<P, O>) => Promise<void>` | The body `cachedModelTest` puts inside `test()`, for a harness that is not Vitest |
| `recording`          | `() => boolean`                                                                | Whether `SMTHRS_RECORD` is `1`                                                    |
| `recordModel`        | `(live: Model, sink: (call: RecordedCall) => void) => Model`                   | Wraps a live model so every request and its events are appended to `sink`         |
| `replayModelError`   | `(error: ModelLikeError) => ModelError`                                        | Rebuilds the `ModelError` a fixture recorded                                      |
| `preparedRequest`    | `Route.PreparedRequest`                                                        | The credential-free route every test seat resolves to                             |
| `RoutedFlow`         | interface                                                                      | One routed flow: `id`, `file`, `spec`, `agent`, `sandbox`, `tools`                |

## @smthrs/create-app/routesBin

The body of the `smithers-routes` executable, exported so a test or another
host can run it and read exactly what a user at a terminal would see.

```ts
const runRoutesBin: (argv: ReadonlyArray<string>, options: RoutesBinOptions) => number
const usage: string
```

`runRoutesBin` returns the process exit code: 0 is a written or clean tree, 1 is
drift under `--check` or a refused tree, and 2 is a flag given without a value.

| `RoutesBinOptions` field | Default         | Meaning                                                                                            |
| ------------------------ | --------------- | -------------------------------------------------------------------------------------------------- |
| `io`                     | required        | `{ out, err }`, one line at a time. The bin binds these to `console.log` and `console.error`       |
| `cwd`                    | `process.cwd()` | The directory `--root` defaults to                                                                 |
| `write`                  | `writeRoutes`   | The router entry point, so a caller can observe a failure the filesystem cannot be made to produce |

The flags are `--check`, `--root`, `--app`, `--flows`, and `--tools`, each
accepting either `--root <dir>` or `--root=<dir>`. The full command reference,
including exit codes and the shim's entry-point choice, is in
[Command reference](https://create-app.smithers.sh/reference/cli/).

## Generated files

`smithers-routes` writes two files at the app root and never anything else.
`routes.gen.ts` holds every flow with its three resolved layers plus the pane
names, and imports no React and no virtual module. `routes.ui.gen.ts` holds the
shell layout, the pages, and the pane components.

```bash
smithers-routes           # write; exit 2 on a flag given no value
smithers-routes --check   # write nothing, exit 1 on drift
```

Import bindings in both generated files are numbered by position, and every
specifier is a JSON string literal. No derivation from a route name is
injective, so `flow_a_b` once served the two distinct flows `a-b` and `a/b` and
the generated module failed to parse while the generator reported success. See
[The generated route tables](https://create-app.smithers.sh/concepts/generated-routes/).

---

# @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://crypto.smithers.sh)

Every name below is exported from the root entry point and from
`@smthrs/crypto/Sha256`. There is no other public module.

```ts
import { Digest, digest, digestSync, Sha256, Sha256Error, Sha256ErrorCode, syncCrypto } from "@smthrs/crypto"
```

## 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://crypto.smithers.sh/guides/hash-in-synchronous-code/#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
  `@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 instead.

## 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://crypto.smithers.sh/concepts/what-a-digest-covers/),
and the guarantees these rules add up to, along with what they do not cover,
are in [the contract](https://crypto.smithers.sh/contract/).

## 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`.

## 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://database.smithers.sh)

The root entry point is driver neutral and bundles for browsers. Each driver is
platform specific and lives at an explicit subpath.

| Import                                | Exports                                                                              |
| ------------------------------------- | ------------------------------------------------------------------------------------ |
| `@smthrs/database`                    | `DurableWriter`, `Migrations`, `DatabaseMetrics`, `UnsupportedBackend` 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                                                              |
| `@smthrs/database/UnsupportedBackend` | the ignored connection-string names                                                  |
| `@smthrs/database/node/NodeDatabase`  | Node only. The `node:sqlite` client layer                                            |
| `@smthrs/database/test/TestDatabase`  | Node only. The in-memory client and writer                                           |

`@smthrs/database/internal/*` is blocked by the export map.

## 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, E | 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.
- **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://database.smithers.sh/concepts/write-boundary/).

### 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://database.smithers.sh/concepts/write-boundary/#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", "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. |
| `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`;
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`.

### 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://database.smithers.sh/guides/count-affected-rows/).

## 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://database.smithers.sh/concepts/migration-ladder/) and
[Add a migration](https://database.smithers.sh/guides/add-a-migration/).

## 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 counter appears 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.

## UnsupportedBackend

The environment half of the SQLite-only contract. Strings only, so it is
browser safe.

### ignoredNames

```ts
const ignoredNames: (environment: Readonly<Record<string, string | undefined>>) => ReadonlyArray<string>
```

The `SMITHERS_*` names 1.0.0-rc.0 ignores: `SMITHERS_TEST_PG_URL` and every
name beginning `SMITHERS_POSTGRES_`. 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_POSTGRES_URL has no effect in 1.0.0-rc.0 (SQLite only)
```

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"`.

Provides the `node:sqlite` SQL client through `@effect/sql-sqlite-node`, and
nothing else. 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.

### NodeDatabaseOptions

```ts
interface NodeDatabaseOptions {
  readonly filename: string
  readonly sqlite?: Omit<SqliteClient.SqliteClientConfig, "filename"> | 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. |
| `sqlite`   | Additional driver configuration. WAL remains enabled unless explicitly disabled.                                                                 |

### UnsupportedDatabaseCode

```ts
const UnsupportedDatabaseCode: Schema.Literals<[
  "unsupported_runtime",
  "unsupported_database_file",
  "database_locked"
]>
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.0, raised as a defect rather
than a typed failure because neither refusal is recoverable at run time.

| Code                        | Refused when                                                    | Message                                                                              |
| --------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `unsupported_runtime`       | `process.versions.bun` is set                                   | `1.0.0-rc.0 runs the durable engine on Node.js >=22.19.0 only`                       |
| `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.0 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`                     |

### 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.

## TestDatabase

Node only. `import * as TestDatabase from "@smthrs/database/test/TestDatabase"`.

### layer

```ts
const layer: Layer.Layer<DurableWriter.DurableWriter | SqlClient.SqlClient>
```

Provides the production Node SQLite client and the production durable writer
over a fresh in-memory 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.

See [Test against a database](https://database.smithers.sh/guides/test-against-a-database/).

---

# @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://engine-store.smithers.sh)

> **Related APIs:** [`@smthrs/artifacts`](/docs/reference/api/artifacts/) · [`@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/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://journal.smithers.sh/concepts/state-event-authority/).
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 only `DurableWriter` backing shipped here is `node:sqlite`, so a browser
composition can import the types and the in-memory helpers but cannot execute
durable flows. See [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.

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. |

### 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)`. |
| `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`.      |

### Required services

`Journal`, `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`, 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.
Restore the original policy and history identity, or choose a new action
identity. No persisted keys or journal producer identities are rewritten.
Changing TTL before a decision exists remains allowed; removing `ttlMs`
retains the existing unbounded path and is not covered by this conflict check.

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.

The driver's periodic sweep re-drives three durable shapes on the heartbeat
cadence: 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.

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.

Full model: [Ownership and fencing](https://engine-store.smithers.sh/concepts/ownership-and-fencing/) and
[Attempts and replay](https://engine-store.smithers.sh/concepts/attempts-and-replay/).

## DurableEngineState

[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. A successful
mutation means the row is durable, so callers may journal and schedule a wake
only after the mutation returns.

| 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>>`                                  |
| `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>>`                          |
| `staleRunningRuns`       | `(staleBeforeMs: number, limit?: number) => Effect<ReadonlyArray<string>>`                   |
| `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>) => Effect<A, E, R>`                                      |

`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. `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: a Postgres-backed
implementation must use `SERIALIZABLE`. The in-memory twin runs the effect
directly, having no crash windows to close.

### 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? }`                                                                        |
| `WaitingRow`              | `{ runId, reason, wakeAt: number \| null, token: string \| null }`                                   |
| `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                        |

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://engine-store.smithers.sh/guides/testing/).

Full model: [Durable waits](https://engine-store.smithers.sh/concepts/durable-waits/).

## 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, artifacts: ArtifactStore.Service, options?: FileSystemOptions) => Service` | The production boundary.                                                                                               |
| `layer`             | `Layer<Service, never, FileSystem \| ArtifactStore>`                                         | Provides it.                                                                                                           |
| `layerTest`         | `(options?: TestOptions) => Layer<Service>`                                                  | Deterministic in-memory boundary.                                                                                      |
| `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. |

### 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://engine-store.smithers.sh/concepts/step-boundaries/).

## 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, never, FileSystem \| ArtifactStore \| Workspace>`          | The filesystem-backed sandbox.                                           |
| `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>
}
```

| 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, artifacts: ArtifactStore.Service, workspaceRoot: string, options?: FileSystemOptions) => Service`                | The production host.                                                                                                        |
| `layerFileSystem`           | `(options?: FileSystemOptions) => Layer<Service, never, FileSystem \| ArtifactStore \| Workspace>`                                 | Provides it, taking the root from the kernel `Workspace`.                                                                   |
| `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? }`, defaulting to 1 MiB and matching `StepBoundary`'s evidence bound                            |

### Errors

| Error                     | Fields                                                                                                                            |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `WorkspaceError`          | `code` of `invalid_path`, `not_found`, `host_unavailable`, or `path_escapes_workspace`; `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://engine-store.smithers.sh/concepts/workspace-transactions/).

## 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.

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://engine-store.smithers.sh/guides/drive-a-plan/).

`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://engine-store.smithers.sh/guides/defer-work-with-selection/).

## 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.

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.

| 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.                                            |
| `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://engine-store.smithers.sh/guides/collect-unreferenced-artifacts/).

## 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 value in either number is read as zero.                                                                                           |
| `RetainReport`            | `cutoffMs`, `runIds`, `retainedForLiveDescendants`, `retainedForLiveAncestors`, `runs`, `attempts`, `clockDeadlines`, `deferredCompletions`, `journalEntries`, `journalCheckpoints`, `archiveEntries`, `timeTravelReceipts`, `dryRun` |
| `Options` (for `collect`) | `olderThanMs` (an absolute epoch millisecond threshold), `dryRun?`, `database?`, `limit?`                                                                                                                                             |
| `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.

`collect` is the pass [`smthrs gc`](/docs/reference/cli/gc/) runs over one database file.

Full guide: [Delete old run history](https://engine-store.smithers.sh/guides/delete-old-run-history/).

## ExecutionSnapshot

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. |
| `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, preserving wake time and token.           |
| `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. Failures use `RunStoreError`
with causes preserved; 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`.

## 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://engine-store.smithers.sh/guides/observe-executions/).

## 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://engine-store.smithers.sh/guides/back-up-and-restore/).

## 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.

## 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.                                               |
| `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                                                                                                                       |
| `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://engine-store.smithers.sh/guides/observe-engine-metrics/).

## 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"`                                                                                   |

`parentExecutionId` is present only on a child run, and `onParentExit` travels
with it: 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.

## 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://engine-store.smithers.sh/troubleshooting/).

## 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://engine-store.smithers.sh/guides/testing/).

## 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.

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://engine-store.smithers.sh/concepts/cache-admission/). See also
[Run durably over SQLite](/docs/tutorials/first-flow/#6-run-durably-over-sqlite)
and [Content addressing](/docs/concepts/content-addressing/).

---

# @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://errors.smithers.sh)

`@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.

## SmithersError

The error class the integration adapters raise, and the only class in the
package.

### class SmithersError

```ts
class SmithersError extends Error {
  constructor(
    code: SmithersErrorCode,
    summary: string,
    details?: Record<string, unknown>,
    options?: SmithersErrorOptions
  )

  declare readonly name: string
  readonly code: SmithersErrorCode
  readonly summary: string
  readonly docsUrl: string
  declare readonly details: Readonly<Record<string, unknown>> | undefined
}
```

Constructor parameters:

| Parameter | Type                      | Meaning                                                                                            |
| --------- | ------------------------- | -------------------------------------------------------------------------------------------------- |
| `code`    | `SmithersErrorCode`       | The machine-readable classification. Required, and closed 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, the reported name, 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. |
| `name`    | `string`                                           | `options.name`, or `"SmithersError"`. Installed as a non-enumerable, writable, configurable own property.                                                        |
| `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`, and, when supplied,
`details`, in that order. `JSON.stringify` and `util.inspect` therefore show
those and nothing else.

The constructor restores the subclass prototype through `new.target`, so
`instanceof` holds for a subclass under a transpiled target.

### SmithersErrorOptions

```ts
interface SmithersErrorOptions {
  readonly cause?: unknown
  readonly includeDocsUrl?: boolean
  readonly name?: string
}
```

| 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. |
| `name`           | `"SmithersError"` | The name the error reports, and the name its stack starts with.                                                                   |

### 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.

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://smithers.sh/docs/reference/errors"
```

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://errors.smithers.sh/reference/error-codes/).

### 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://errors.smithers.sh/reference/error-codes/): every code with its raise
  sites and the caller's move.
- [The shape of a SmithersError](https://errors.smithers.sh/concepts/error-shape/): the guarantee
  behind each field.
- [`@smthrs/integrations`](/docs/reference/api/integrations/): the only consumer, and the source
  of the `IntegrationError` and `TelegramApiError` subclasses.

---

# @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://evals.smithers.sh)

> **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://evals.smithers.sh/installation/).

## 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 { Flow } from "@smthrs/core"
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 = Flow.make({ 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.

## 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)`.
- **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.** Validation requires the artifact's
  top-level `suite` field and a `suite` on every record, so ownership is recorded
  even when the baseline has no records. Comparison refuses an artifact or any
  record that names a suite other than the run's: a mismatch fails with
  `invalid_baseline` rather than reporting a clean pass.

## 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                |
| `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.

## Determinism and limits

- `Runner.run` takes `runId` and `at` from the caller and stamps every
  observation with them, so two runs over the same inputs produce identical
  observations and identical `Report.json` bytes.
- 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.
- `Report.markdown` escapes, flattens, and caps every cell at 240 characters,
  including the heading.

## 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
eight 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 when the binding
  declares none of its own. 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.

### 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.
- `CaseInput`: the `Case` the executor receives.
- `Run`: `(suiteCase: CaseInput) => Effect.Effect<Execution, EvalError>`.
- `Service`: `{ run: Run }`.
- `Implementation`: `{ run }` or `{ execute }`. The union accepts exactly one
  of them, so an object supplying both is a type error instead of a service
  whose two halves can disagree.
- `CaseExecutor`: the `Context.Service` tag, `flows/evals/CaseExecutor`.

```ts
const make = (implementation: Implementation | Run): Service
```

Builds an executor from a callback, or from an object naming it `run` or
`execute`. Throws a `TypeError` when neither is 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.

```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, a
non-string identity field, or a score that is not finite in [0, 1].

```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. `load` parses and validates canonical baseline JSON,
failing with `invalid_baseline`.

### 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.

```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 json = (report: Regression.Report): string
const markdown = (report: Regression.Report): string
```

`json` serializes a regression report as stable, sorted-key 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, 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. Cell
text is escaped, stripped of control characters, and capped, so a suite, case,
or scorer name cannot inject Markdown into a rendered report.

### 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.

## 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.                   |
| `CaseExecutor.Execution`        | models        | The result of executing one target-flow case.                                       |
| `CaseExecutor.CaseInput`        | models        | Input accepted by a case executor.                                                  |
| `CaseExecutor.Run`              | models        | The one callback a case executor is.                                                |
| `CaseExecutor.Service`          | services      | Runtime shape for an injectable target-flow executor.                               |
| `CaseExecutor.Implementation`   | models        | Implementation accepted by `make`.                                                  |
| `CaseExecutor.CaseExecutor`     | services      | Injectable execution boundary for a target flow.                                    |
| `CaseExecutor.make`             | constructors  | Builds an executor from a callback, or from an object naming it `run` or `execute`. |
| `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.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.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.                                    |

## A worked suite

[`evals/agent`](https://github.com/smithersai/smithers/tree/main/evals/agent)
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://evals.smithers.sh/guides/gate-a-run-in-ci/).

---

# @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://flows.smithers.sh)

> **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/platform-node`](/docs/reference/api/platform-node/) · [`@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 three public entry points.

| Entry point                   | Contents                                                                           |
| ----------------------------- | ---------------------------------------------------------------------------------- |
| `@smthrs/flows`               | The barrel: nineteen engine packages re-exported, plus `namespaces`. Browser-safe. |
| `@smthrs/flows/NodeRuntime`   | The durable runtime compositions for Node and local SQLite. Node-only.             |
| `@smthrs/flows/SandboxedFlow` | Running a child flow's own code inside a provisioned machine. Node-only.           |

```ts
import { Action, Flow, Kernel, RunStore } from "@smthrs/flows"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import * as SandboxedFlow from "@smthrs/flows/SandboxedFlow"
```

`@smthrs/flows/internal/*` and `@smthrs/flows/*/index` are blocked in the export
map and are not public.

Durable execution is supported on Node.js 22.19.0 or later with local SQLite.
The root entry point bundles for browsers, which buys authoring and inspection;
it does not make a browser a durable host, and supplying another SQL client does
not change that.

## 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/)                   |
| `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, static effect
declarations, and plan storage; `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 fourteen authoring names
sit at the top level: `Action`, `DurableClock`, `DurableDeferred`,
`DurableQueue`, `Flow`, `FlowRuntime`, `Graph`, `HumanTask`, `Interpreter`,
`Poll`, `RetryPolicy`, `Sleep`, `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.

The `@smthrs/platform-*` bundles are deliberately absent. A program chooses
[`@smthrs/platform-node`](/docs/reference/api/platform-node/),
[`@smthrs/platform-bun`](/docs/reference/api/platform-bun/), or
[`@smthrs/platform-browser`](/docs/reference/api/platform-browser/) 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.

## NodeRuntime

```ts
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
```

The supported Node composition. Importing this module opens `node:sqlite`
through `@smthrs/database/node/NodeDatabase`, which is why the browser-safe root
does not re-export 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`.

| 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.                                                                                                                                                                                                       |
| `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. |

`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. To preserve foreign-host owners, use `HostLiveness.isAlive({ hostId })`
from [`@smthrs/platform-node`](/docs/reference/api/platform-node/), 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.

### `RuntimeConfigurationError`

```ts
class RuntimeConfigurationError extends Schema.TaggedError("@smthrs/flows/RuntimeConfigurationError")({
  code: "invalid_runtime_configuration"
  field: string
  message: string
})
```

Thrown synchronously by `storage`, `make`, `layer`, and `layerHost` 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.

### `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 `Executable.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` and `layer` are overloaded on the registry argument rather than defaulting
it, because a default cannot honor a caller-chosen registry type. Naming a
registry type without passing its layer does not compile.

### `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.                                                                                                                                                                 |
| `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.                                                                                                                                        |

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 from [`@smthrs/platform-node`](/docs/reference/api/platform-node/) 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.

### `CompositionRootsAreComplete`

```ts
type CompositionRootsAreComplete
```

A compile-time assertion type that pins each composition root to its documented
host boundary: `make` requires `Crypto | FileSystem | Jj | Scope`, `layer`
requires `Crypto | FileSystem | Jj`, and `layerHost` requires nothing. It has no
runtime value. It records the boundary each root promises, and a change to one
of those boundaries stops compiling instead of reaching you as a missing service
at run time.

## SandboxedFlow

```ts
import * as SandboxedFlow from "@smthrs/flows/SandboxedFlow"
```

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 and it starts a guest runtime.

```ts
const run = Effect.gen(function*() {
  const result = yield* SandboxedFlow.execute(Child, { n: 31 }, {
    provider,
    session: `child:${executionId}`,
    entry: new URL("./child.ts", import.meta.url),
    runtime: "node",
    collectDiff: true
  })
  // result.output is Child's success value, decoded through Child's own schema.
  // result.diff is the files the guest created, as { path, bytes } data.
  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://flows.smithers.sh/concepts/runner-protocol/).

### `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"
    | "deadline_exceeded"
  message: string
  cause?: unknown
})
```

The one failure type every 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 wrote one that is not the protocol's JSON.                    |
| `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 workspace diff exceeds `Limits.files` or `Limits.diffBytes`.                                         |
| `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.

### `Limits` and `ResolvedLimits`

```ts
interface Limits {
  readonly resultBytes?: number | undefined
  readonly diffBytes?: number | undefined
  readonly files?: number | undefined
}

interface ResolvedLimits {
  readonly resultBytes: number
  readonly diffBytes: number
  readonly files: number
}
```

Bounds on what comes back from the guest. An omitted or `undefined` bound keeps
its default.

### `defaultLimits`

```ts
const defaultLimits: ResolvedLimits
```

| Bound         | Default | What it caps                                      |
| ------------- | ------- | ------------------------------------------------- |
| `resultBytes` | 5 MiB   | The result JSON the guest wrote.                  |
| `diffBytes`   | 100 MiB | The total bytes collected across the diff.        |
| `files`       | 1,000   | The number of created or resized files collected. |

### `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.                                                                    |
| `collectDiff` | `boolean`          | `false`         | Whether to collect the files the guest created or resized.                                                                                                                                                                     |
| `limits`      | `Limits`           | `defaultLimits` | Bounds on the result and the diff.                                                                                                                                                                                             |
| `timeout`     | `Duration.Input`   | 10 minutes      | The wall-clock budget for the whole session, acquisition through result readback. Measured on the platform timer, so it fires under a frozen test clock too.                                                                   |

### `DiffEntry`, `Diff`, and `Result`

```ts
interface DiffEntry {
  /** The path relative to the session workdir. */
  readonly path: string
  readonly bytes: Uint8Array
}

interface Result<A> {
  readonly output: A
  readonly diff: ReadonlyArray<DiffEntry>
}

const DiffEntry: Schema.Struct<{ path: Schema.String; bytes: Schema.Uint8Array }>
const Diff: Schema.Array<typeof DiffEntry>
```

One `DiffEntry` per file the guest created or resized, as it stood when the guest
exited. The schemas are JSON-encodable for the journal: the bytes serialize as
base64.

### `resultSchema` and `ResultSchema`

```ts
type ResultSchema<Success extends Schema.Top> = Schema.Struct<{
  readonly output: Success
  readonly diff: typeof Diff
}>

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"]>, SandboxedFlowError>
```

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.

Diff change detection compares sizes by path against a snapshot taken before the
guest ran. A created file and a file whose size changed are collected; a file
rewritten in place at its previous size on a reattached workspace is the one edit
it misses. A fresh workspace holds nothing but the protocol's own files, so every
file the child writes there is a creation.

### `SandboxedAction` and `action`

```ts
type SandboxedAction<Tag, Payload, Success> = Action.Declared<
  Tag,
  Payload,
  ResultSchema<Success>,
  typeof SandboxedFlowError
>

const action: <Tag, Payload, Success, Error, Requires>(
  flow: Flow.Flow<Tag, Payload, Success, Error, Requires>,
  options?: { readonly name?: string | undefined }
) => SandboxedAction<string, 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 `SandboxedFlowError`. The tag is
`<flow tag>/sandboxed` unless `options.name` says otherwise.

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
}
```

What `toLayer` hands an options function: the decoded payload of the call and
the parent execution's id, which is the natural material for a session key that
is exclusive per execution and stable across a resume.

### `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 }) => ({
  provider,
  session: `child:${executionId}`,
  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.

## See also

Each namespace in the table above has its own API reference, reached through the
links there. [`@smthrs/cli`](/docs/reference/api/cli/) is this engine as the `smthrs` command
line, which builds its own durable engine through `NodeRuntime`. Depend on
individual `@smthrs` packages rather than this barrel when a smaller dependency
surface is worth the extra imports.

---

# @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://fs.smithers.sh)

> **workspace-private:** `@smthrs/fs` is documented for contributors and is not installable from npm.

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@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.

## 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 and every value it
returns 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>` | Loads and invokes an exact named route using snapshotted input.                        |

`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` snapshots its input to inert JSON before any module-loading await, so
mutating the caller's object afterwards cannot change an in-flight
invocation. A name that does not match exactly, extra path segments
included, fails with `unknown_command`.

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. A `ui.tsx` beside a flow is recorded on the route's
`ui` field; 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 schema-decoded, frozen input.                |

### `Service`

Executes a materialized flow.

| Field    | Type                                                          | Description                 |
| -------- | ------------------------------------------------------------- | --------------------------- |
| `invoke` | `(invocation: Invocation) => Effect.Effect<unknown, FsError>` | Runs one materialized flow. |

### `FlowInvoker`

```ts
class FlowInvoker extends Context.Service<FlowInvoker, Service>()("/fs/FlowInvoker")
```

The flow invocation service tag, keyed `"/fs/FlowInvoker"`.

### `FlowInvoker.make`

```ts
;((implementation: Service) => Service)
```

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<Service>) => Service)
```

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<Service>) => 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, also
exported as a `Schema.Literals` value for decoding. For the meaning and
trigger of each code, see the
[error codes table](https://fs.smithers.sh/contract/#error-codes).

### `FsError`

```ts
class FsError extends Schema.TaggedError<FsError>()("flows/fs/FsError", {
  code: Code,
  method: Schema.String,
  description: Schema.String,
  path: Schema.optional(Schema.String)
})
```

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.                                       |
| `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 the same way.
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`.

---

# @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://gateway.smithers.sh)

> **Related APIs:** [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/run-store`](/docs/reference/api/run-store/) · [`@smthrs/sync`](/docs/reference/api/sync/)

The gateway requires `effect`, `@effect/platform-node`, and
`@effect/platform-node-shared` as exact `4.0.0-rc.112` peers. The shared
platform pin keeps npm from selecting a later release candidate under the
Node platform's transitive range. Use the same Effect version in the host.

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://gateway.smithers.sh/concepts/projections/),
[Subscriptions and cursors](https://gateway.smithers.sh/concepts/subscriptions/), and
[The trust boundary](https://gateway.smithers.sh/concepts/trust-boundary/).

## `GatewayServer`

The whole HTTP surface as one application layer a host serves.

### Mounts

`GatewayServer.layer` mounts seven routes, and `NodeGateway.layer` binds them
to a socket.

| 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`                          |

### Types and constants

| Export                       | Signature                                                                                            | Meaning                                                                                                     |
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `Health`                     | `Schema.Struct` and its type                                                                         | What `GET /health` answers: `GatewaySchema.GatewayHealth` plus the `version` of the package serving it.     |
| `LayerOptions`               | `{ heartbeatMillis?: number; ingress?: IngressOptions }`                                             | How an assembled gateway is configured.                                                                     |
| `IngressOptions`             | `{ maxRequestBodyBytes?: number; loopbackOnly?: boolean; authorize?: (headers) => Effect<boolean> }` | The ingress policy the RPC mounts run behind.                                                               |
| `rpcPaths`                   | `ReadonlyArray<string>`                                                                              | `["/rpc", "/projections", "/sync"]`: the `POST` mounts that carry RPC request messages.                     |
| `protectedPaths`             | `ReadonlyArray<string>`                                                                              | `["/projections", "/sync", "/rpc/ws", "/projections/ws", "/sync/ws"]`: paths that pass edge authentication. |
| `defaultMaxRequestBodyBytes` | `number`                                                                                             | 1,048,576. The default maximum request body accepted by an RPC mount.                                       |
| `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://gateway.smithers.sh/concepts/trust-boundary/).

### 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.                                      |

`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 & { allowedHosts?: readonly string[]; listen?: boolean; credential?: string; heartbeatMillis?: number; maxRequestBodyBytes?: number }` | Bind address, explicitly admitted Host names, authentication, and request limits.                             |
| `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. |
| `layerAuth`            | `(options: ServerOptions) => Layer<ControlRpcs.ControlAuth>`                                                                                          | Bearer authentication when a credential is configured, and the loopback-only local operator when none is.     |
| `bearerPrincipal`      | `Readonly<{ id: string; kind: string }>`                                                                                                              | The frozen `{ id: "gateway", kind: "bearer" }` identity stamped by shared-bearer authentication.              |
| `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://gateway.smithers.sh/guides/serve-beyond-loopback/).

Without that bearer, `NodeGateway.layer` enables `loopbackOnly`: every request
must carry a loopback `Host`, and a browser `Origin` must use `http` or `https`
on `localhost`, `127.0.0.1`, or `[::1]`, with an optional port. 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`        | `(selector: ProjectionSelector) => Effect<ProjectionSnapshot, GatewayError>`                         | Every row the selector currently projects, and the cursor they were read at.                                       |
| `Service.subscribe`       | `(selector: ProjectionSelector, after?: ProjectionCursor) => Stream<GatewayFrame, 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. Equals `ControlSchema.maxPageSize`.                                  |
| `maxEventsPerRun`         | `10_000`                                                                                             | The most journal events one run projection admits.                                                                 |
| `maxProjectionBytes`      | `4 * 1024 * 1024`                                                                                    | The largest encoded event history, or projected row set, one run admits.                                           |

`ControlService` is `@smthrs/control` `Control`'s service interface, the shape
the tag carries.

## `GatewaySchema`

The wire schemas the read path, its subscriptions, and the singleton lifecycle
speak. Every entry is an `effect` `Schema` with a same-named type.

### Identity

| Export          | Fields                                          |
| --------------- | ----------------------------------------------- |
| `Workspace`     | `workspaceHash`, `workspacePath`                |
| `GatewayConfig` | `workspace`, `host`, `port`, `protocolVersion`  |
| `GatewayStatus` | `running`, `url`, `gatewayId`, `startedAtMs`    |
| `GatewayHealth` | `workspaceHash`, `gatewayId`, `protocolVersion` |

### Selectors

| Export                  | Shape                                    |
| ----------------------- | ---------------------------------------- |
| `ProjectionName`        | the seven 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 }` |
| `ProjectionSelector`    | the union of the seven                   |

`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.

`rowSchemaFor(selector: ProjectionSelector)` answers the schema of the rows that
selector projects, so a client decodes a snapshot instead of casting it.

### Cursors, snapshots, and frames

| 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.
`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.

### Singleton and tokens

| Export            | Shape                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------- |
| `SingletonRecord` | `{ gatewayId, workspaceHash, hostId, pid, url, protocolVersion, startedAtMs, sessionToken }` |
| `TokenScope`      | `"sync" \| "control" \| "tokens" \| "admin"`                                                 |
| `TokenRecord`     | `{ id, workspaceHash, label, scopes, digest, createdAtMs, expiresAtMs, revokedAtMs? }`       |

See [Declared but not served](#declared-but-not-served).

## `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`, `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) => RunSummaryRow`             | Status comes from the run row, everything else from the events.                                             |
| `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.               |

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`, `PersistenceError`, `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://gateway.smithers.sh/guides/submit-an-approval/).

## `Diagnosis`

What happened to a run, computed from that run's own control events. The
vocabulary matches `@smthrs/cli` `Forensics`: this module is that rendering,
re-expressed as a served projection rather than a terminal card.

| 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) => Digest`                          | The facts. Total: an unknown kind contributes nothing, including its timestamp.                                                    |
| `verdict`   | `(value: Digest) => string`                   | One line: the status plus the reason that most explains it.                                                                        |
| `duration`  | `(value: Digest) => string`                   | The wall-clock span the handled events cover, as `12s` or `3m 04s`.                                                                |
| `render`    | `(subject: Subject, value: Digest) => string` | The whole card: verdict, activity evidence, tokens, refusals, cause, and output.                                                   |
| `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://gateway.smithers.sh/guides/diagnose-a-run/).

## `GatewayError`

| Export             | Signature                                                                 | Meaning                                                                                     |
| ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GatewayErrorCode` | nine literals                                                             | The whole failure vocabulary.                                                               |
| `GatewayError`     | `Schema.TaggedError` tagged `flows/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 local-only ingress guard, when `Host` does not name `localhost`, `127.0.0.1`, or `[::1]`                                                             |
| `invalid_origin`    | 403    | the local-only ingress guard, when a supplied browser `Origin` does not name HTTP(S) on a loopback 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. The whole cause is logged server-side instead, because
this error is the RPC error schema, so anything left on it is serialized to
every bearer holder and forwarded to a browser by a relay.

## `SuperviseRuntime`

The host seam a supervisor would implement to recover abandoned work.

| Export                  | Signature                                                                                                                | Meaning                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `StaleRunningCandidate` | `{ _tag: "stale-running", run, livenessEvidence }`                                                                       | A running run whose owner is proven dead.                  |
| `QuotaDueCandidate`     | `{ _tag: "quota-due", run, resetAtMs }`                                                                                  | A quota-parked run whose reset time has arrived.           |
| `StaleClaimCandidate`   | `{ _tag: "stale-claim", run, claimantDeathEvidence }`                                                                    | A run whose unactivated claim holder is proven dead.       |
| `Candidate`             | the union of the three                                                                                                   | A run supervision may recover or resume.                   |
| `ResumeLease`           | `{ runId, claimant, candidate }`                                                                                         | A fenced request to resume one candidate.                  |
| `ResumeErrorCode`       | `"claim_lost" \| "resume_failed"`                                                                                        | Stable resume failures.                                    |
| `ResumeError`           | `Schema.TaggedError` tagged `flows/gateway/ResumeError`                                                                  | `{ code, message, cause }`.                                |
| `Service`               | `{ scan: (now: number) => Effect<ReadonlyArray<Candidate>>; resume: (lease: ResumeLease) => Effect<void, ResumeError> }` | Engine-facing supervision operations.                      |
| `SuperviseRuntime`      | `Context.Service` tagged `flows/gateway/SuperviseRuntime`                                                                | The service tag.                                           |
| `make`                  | `(service: Service) => Service`                                                                                          | Constructs a supervision runtime.                          |
| `makeNoop`              | `(overrides?: Partial<Service>) => Service`                                                                              | No candidates, successful resumes, overridable per member. |
| `layerNoop`             | `(overrides?: Partial<Service>) => Layer<SuperviseRuntime>`                                                              | Provides that no-op.                                       |

A candidate names a run by its `@smthrs/control` `RunSummary` rather than by a
store row, which keeps the promise the rest of the package makes: a projection
is the contract and a store row is an implementation detail.

This release ships `make`, `makeNoop`, and `layerNoop` only. Nothing in the
package implements the seam, so unless a host passes its own `Service` the port
does nothing. Recovery is a reclaim rather than a supervisor: a running engine
process with the flow registered takes over a run whose owner stopped renewing
its heartbeat. See [Recovery](https://gateway.smithers.sh/troubleshooting/#recovery).

## `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/).

## `test/TestSuperviseRuntime`

A controllable in-memory supervision runtime for tests.

| Export                        | Signature                                                                                                         | Meaning                                                |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `TestSuperviseRuntimeOptions` | `{ candidates?: ReadonlyArray<Candidate>; resumeError?: ResumeError }`                                            | Its initial configuration.                             |
| `TestSuperviseRuntime`        | `{ runtime, resumes, setCandidates, setResumeError }`                                                             | The runtime plus the recorded leases and the controls. |
| `make`                        | `(options?: TestSuperviseRuntimeOptions) => TestSuperviseRuntime`                                                 | Constructs one.                                        |
| `layer`                       | `(options?: TestSuperviseRuntimeOptions, onReady?: (t: TestSuperviseRuntime) => void) => Layer<SuperviseRuntime>` | Provides one and hands the controls to `onReady`.      |

See [Test against a real gateway](https://gateway.smithers.sh/guides/testing/).

## Declared but not served

`GatewaySchema.Workspace`, `GatewayConfig`, `GatewayStatus`, `SingletonRecord`,
`TokenScope`, and `TokenRecord` describe a workspace singleton handshake this
release has no route for. No code here mints, reads, persists, or serves one.
Read them as a proposal, not as a contract a client can call.

Everything else the schema declares is served. `ProjectionName` in particular
equals the set the read path answers.

---

# @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://harness.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/core`](/docs/reference/api/core/) · [`@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/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.

## Entry points

| Import                           | What it is                                                                                                           |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `@smthrs/harness`                | The root barrel: the 26 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

27 public modules, 336 documented exports. Each module's full export table is
in the [module and export inventory](https://harness.smithers.sh/reference/); 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`                 | `DisciplineArmed`, `TurnOpened`, `ModelDelta`, `ModelRetried`, `ModelSettled`, `CellProduced`, `CellRejectedInFrame`, `CellCallStarted`, `CellCallSettled`, `CellPrinted`, `CellSettled`, `TransitionApplied`, `ReadOnlyDemandIssued`, `ReadOnlyDemanded`, `RepeatDemanded`, `NarrowedDemanded`, `UnmovedDemanded`, `UnresolvedDemanded`, `NarrowOnlyDemanded`, `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`, `CaptureRequest`, `EngineLike`, `make`, `layer`, `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`, `ContextWindowErrorCode`, `ContextWindowError`, `Segment`, `ContextWindow`, `SegmentInput`, `MakeOptions`, `makeSegment`, `make`, `empty`, `appendTurn`, `activateTools`, `prefixDigest`, `compactPrefix`, `compact`, `render`, `contextWindowTokensFor`                                                                                                                                                                                                                                                                                             | 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.                                                      |
| `Compaction`                 | `summaryInstruction`, `InvalidStep`, `Summarizer`, `CompactionStep`, `TokenAccounting`, `shouldCompact`, `selectPrefix`, `declare`, `summaryRequest`, `apply`                                                                                                                                                                                                                                                                                                                                                                                                                                           | Declarations for sealed transcript-summary steps.                                                        |
| `Steering`                   | `Delivery`, `SteerInsert`, `QueueInsert`, `Insert`, `SeatChange`, `ThinkingChange`, `ActivateTools`, `Item`, `Queue`, `Drain`, `BoundaryInput`, `DrainRecord`, `drainRecord`, `PromotionState`, `empty`, `enqueue`, `drainAtClose`, `promoteAtIdle`, `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`, `Latch`, `latch`, `driveCell`, `raisedOutcome`                                                             | The deterministic script sandbox port.                                                                   |
| `CellTurn`                   | `defaultMaxFrames`, `defaultReadOnlyFrames`, `defaultModelCallMs`, `defaultRepeatFrames`, `defaultNarrowingDemands`, `defaultUnmovedDemands`, `defaultUnresolvedDemands`, `defaultRevalidations`, `defaultMaxCheckpoints`, `State`, `Input`, `make`, `teach`, `run`                                                                                                                                                                                                                                                                                                                                     | The cell-first controller.                                                                               |
| `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`, `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.                                                                         |
| `UnmovedTree`                | `Unmoved`, `find`, `demand`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | The completion with nothing behind it.                                                                   |
| `UnresolvedFailure`          | `exitStatusKey`, `failed`, `passed`, `Displaced`, `revisits`, `find`, `demand`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | The failing check a completion stepped around.                                                           |
| `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.                                           |
| `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.                                                |
| `callMs`      | 120,000 | per call    | Wall-clock time one flow call may take before it settles as a catchable timeout.                                                       |

`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. The
reading is cleared with the refusal, because freeing is itself done by a cell.

## Bytes

Every bound this package states in bytes is measured in UTF-8 bytes by one
shared helper, and every elision notice states the real number.
The print channel, the retention ceiling, the call ledger's line sizes and the
memory probe all read the same unit, so a CJK or emoji payload is bounded by
what it actually costs rather than by how many UTF-16 code units it happens to
occupy.

## 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.

## 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"`. The tool-shaped members that remain
  on `Steering`, `ContextWindow`, `Compaction` and `AgentEvent.TurnOpened` are
  reserved for a future foreign-adapter loop and carry no compatibility promise.
- 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? }`, where `flows` is the frame's
`FlowDescriptor` list, already narrowed by seat visibility.

`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 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 read-only frames before intervention.  |
| `defaultModelCallMs`       | 300,000 | Wall-clock milliseconds one model call may spend.  |
| `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. |
| `defaultRevalidations`     | 1       | In-frame answers to an unparseable cell.           |
| `defaultMaxCheckpoints`    | 8       | Trees one run may pin with `ctx.checkpoint()`.     |

`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 refuses the transition in-frame.

`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, and the context window. 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)` prepends the cell contract and the
callable-flow catalog to a context window as prefix segments, so the teaching
is stable for the life of the run.

## 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, mint?, bounded?, limits? }`;
`RealmFrame` is `{ outcome, prints, bindings }`. The `Sandbox.Handler`
resolves one `Sandbox.Invocation` (`{ ordinal, flow, input, at? }`) into a
`Cell.CallResult`; 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.latch` creates the wake-up latch it waits on;
`Sandbox.PendingCall` is the queued-call shape. `Sandbox.callTimedOut(flow,
callMs)` synthesizes the catchable `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).

## 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://harness.smithers.sh/guides/workerd/).

## 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.

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)` hashes the complete material
declaration: every top-level `FlowDescriptor` field except `provenance.pack`,
with `capabilities` sorted and every other array in declaration order.

**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://harness.smithers.sh/troubleshooting/#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://agent.smithers.sh/concepts/engine-port/#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 splice: (batch: Plan.Batch) => Stream.Stream<Plan.SpliceEvent, HarnessError>
  readonly call: (call: Cell.Call) => Effect.Effect<Cell.CallResult, HarnessError>
  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 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.
- `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.
- `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.
- `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.
- `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, refused 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), `SeatChange` and `ThinkingChange` (applied only after the
current turn closes), and `ActivateTools` (an additive tool update).
`Steering.empty`, `enqueue`, `drainAtClose(queue, cutoff)`, and
`promoteAtIdle(state)` operate on it without mutation. `Steering.Drain` is
what one boundary promoted; `Steering.DrainRecord` and `drainRecord` project
it into its journaled record.

```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. 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>
```

`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.

## 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` the cell can catch.

```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
```

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 the call's input (retrying once with explicit `null` keys
dropped, the JavaScript reading of an omitted optional key), executes the
handler, and encodes the output back into serializable JSON. Correctable
failures (`invalid_input`, `flow_failed`) settle as `failure` results the cell
catches; a permission requirement, a denial, or a harness-level failure is
escalated into the error channel as a `HarnessError` of code `suspended`, and
an interruption is never caught at all.

`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 29. The controller
journals them in order: `DisciplineArmed` once at the start, the frame cycle
(`TurnOpened`, `ModelDelta`, `ModelRetried`, `ModelSettled`, `CellProduced`,
`CellRejectedInFrame`, `CellCallStarted`, `CellCallSettled`, `CellPrinted`,
`CellSettled`, `TransitionApplied`), the interventions and observations
(`ReadOnlyDemandIssued`, `ReadOnlyDemanded`, `RepeatDemanded`,
`NarrowedDemanded`, `NarrowOnlyDemanded`, `UnmovedDemanded`,
`UnresolvedDemanded`, `SufficiencyObserved`, `VacuousVerificationObserved`,
`MutationObserved`, `CheckpointMinted`, `CompactionSettled`,
`SteeringDrained`), and the terminal set (`Suspended`, `PermissionRequired`,
`TurnClosed`, `Resolved`, `Aborted`).

`AgentEvent.eventType` maps every tag to its journal event type, the one
table `CellTurn` writes and `Transcript` reads:

| Tag                           | Journal event type                               |
| ----------------------------- | ------------------------------------------------ |
| `aborted`                     | `flows.harness.aborted.v1`                       |
| `cellCallSettled`             | `flows.harness.cell-call-settled.v1`             |
| `cellCallStarted`             | `flows.harness.cell-call-started.v1`             |
| `cellPrinted`                 | `flows.harness.cell-printed.v1`                  |
| `cellProduced`                | `flows.harness.cell-produced.v1`                 |
| `cellRejectedInFrame`         | `flows.harness.cell-rejected-in-frame.v1`        |
| `cellSettled`                 | `flows.harness.cell-settled.v1`                  |
| `checkpointMinted`            | `flows.harness.checkpoint-minted.v1`             |
| `compactionSettled`           | `flows.harness.compaction-settled.v1`            |
| `disciplineArmed`             | `flows.harness.discipline-armed.v1`              |
| `modelDelta`                  | `flows.harness.model-delta.v1`                   |
| `modelRetried`                | `flows.harness.model-retried.v1`                 |
| `modelSettled`                | `flows.harness.model-settled.v1`                 |
| `mutationObserved`            | `flows.harness.mutation-observed.v1`             |
| `narrowOnlyDemanded`          | `flows.harness.narrow-only-demanded.v1`          |
| `narrowedDemanded`            | `flows.harness.narrowed-demanded.v1`             |
| `permissionRequired`          | `flows.harness.permission-required.v1`           |
| `readOnlyDemandIssued`        | `flows.harness.read-only-demand-issued.v1`       |
| `readOnlyDemanded`            | `flows.harness.read-only-demanded.v1`            |
| `repeatDemanded`              | `flows.harness.repeat-demanded.v1`               |
| `resolved`                    | `flows.harness.resolved.v1`                      |
| `steeringDrained`             | `flows.harness.steering-drained.v1`              |
| `sufficiencyObserved`         | `flows.harness.sufficiency-observed.v1`          |
| `suspended`                   | `flows.harness.suspended.v1`                     |
| `transitionApplied`           | `flows.harness.transition-applied.v1`            |
| `turnClosed`                  | `flows.harness.turn-closed.v1`                   |
| `turnOpened`                  | `flows.harness.turn-opened.v1`                   |
| `unmovedDemanded`             | `flows.harness.unmoved-demanded.v1`              |
| `unresolvedDemanded`          | `flows.harness.unresolved-demanded.v1`           |
| `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.Defect())
}) {}
```

`HarnessErrorCode` is `assembly_failed`, `render_failed`,
`projection_failed`, `model_failed`, `engine_failed`, `read_only_cap`,
`aborted`, or `suspended`. The set is closed to codes this package and
`@smthrs/agent` actually raise; a foreign CLI adapter declares its own family
beside the adapter rather than borrowing this one. `cause` is a
`Schema.Defect` so a live `Error` attached as cause still encodes to JSON for
the durable exit schema.

## 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`), and returns the first the schema
accepts. 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: 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 and its ordered tool results; `activateTools` adds
tools permanently for the window lineage; `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. `render` projects the window into the `ModelRequest` of
[`@smthrs/model`](/docs/reference/api/model/).

`ContextWindow.contextWindowTokensFor(modelId)` 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.

## 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.

## 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; `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.

## 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. 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.

## 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.

## UnresolvedFailure

`import * as UnresolvedFailure from "@smthrs/harness/UnresolvedFailure"`

The failing check a completion stepped around. `failed` and `passed` read a
settled call's `exitStatusKey` (`exitCode`) 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.

## 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://integrations.smithers.sh)

> **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/)

The package exports four namespaces from the aggregate entry point
`@smthrs/integrations` and from the per-provider subpaths
`@smthrs/integrations/core`, `/github`, `/linear`, and `/telegram`. The
`Environment` module is reachable as `@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.
- Failing Effect values fail with `Core.IntegrationError`, except the
  Telegram client, which fails with `Telegram.TelegramClient.TelegramApiError`.
- 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.

## 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.

| Export                    | Signature                                                   | Notes                                                                                                                                                                                       |
| ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verifySignature`         | `(options: VerifyOptions) => boolean`                       | Accepts GitHub's `sha256=<hex>`, a bare hex digest, and a base64 digest. Returns `false`, never throws, 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) => 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://integrations.smithers.sh/concepts/control-plane/) explains
the contract.

| 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.

`SecretResolver` is
`(credential: Redacted<CredentialRef>) => Effect<Redacted<string>, Unauthorized>`.

### Core.ExternalEvent

The normalized event every source produces. Fields: `source`, `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>`. `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, and `details.outcomeUnknown` says 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.                                                                                |

| 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://integrations.smithers.sh/concepts/durable-actions/) explains why a schema and
not the class.

| Export                 | Signature                                                    | Notes                                                                                                                                                                                                   |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IntegrationFailure`   | `Schema.TaggedError`, tag `/integrations/IntegrationFailure` | Fields: `reason`, `message`, `retryable`, optional `outcomeUnknown`, optional `deliveredMessageIds`.                                                                                                    |
| `fromIntegrationError` | `(error: unknown) => IntegrationFailure`                     | 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: IntegrationFailure) => IntegrationError`          | Converts back to the class, preserving `retryable`, `outcomeUnknown`, and `deliveredMessageIds` 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.                                                                                                                                                                     |
| `MAX_MESSAGE_LENGTH`   | `512`                                                        | The longest provider text a failure persists.                                                                                                                                                           |

### Core.Pkce

RFC 7636 PKCE parameters for the GitHub and Linear OAuth apps. All three
constructors throw `TypeError` or `RangeError` for invalid arguments.

| Export                | Signature                           | Notes                                                                                          |
| --------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------- |
| `createPkcePair`      | `(byteLength?: number) => PkcePair` | A fresh verifier with its S256 challenge.                                                      |
| `createCodeVerifier`  | `(byteLength?: number) => string`   | 32 to 96 bytes of entropy, producing the 43 to 128 characters RFC 7636 allows. Defaults to 32. |
| `deriveCodeChallenge` | `(codeVerifier: string) => string`  | 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-HTTP(S) endpoint, 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 `http:` or
`https:` URL; 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.

## 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. Callers that require account isolation pass an environment record explicitly instead. |
| `ambientWorkingDirectory` | `() => string`                                        | Reads the ambient working directory. `ListenerRegistry.reconcile` defaults its workspace root through this.                  |

## 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`), `apiBaseUrl` (falls back to `SMITHERS_GITHUB_API_BASE_URL`,
default `https://api.github.com`), `webhookSecret` (falls back to
`SMITHERS_GITHUB_WEBHOOK_SECRET`), and `maxRetries` (defaults to 3).

| Export                 | Signature                                               | Notes                                |
| ---------------------- | ------------------------------------------------------- | ------------------------------------ |
| `resolve`              | `(config?: GitHubConfig, env?) => ResolvedGitHubConfig` | First non-empty value wins, trimmed. |
| `DEFAULT_API_BASE_URL` | `"https://api.github.com"`                              | The public REST endpoint.            |

### GitHub.GitHubClient

The REST client. Rate-limit handling, 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.

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 retried for every method, waiting the server's `Retry-After` or `x-ratelimit-reset` capped at one minute. A 5xx or transport failure is retried only for a read; on a write it reports `outcomeUnknown` unless `retryUnsafeWrites` is set. An unserializable body or an unparseable path fails `invalid-config` before any request. Interrupting the fiber aborts the request in flight. |
| `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.                                                                                                                                                                                                                                                                   |

| Export                | Signature                                                      | Notes                                                                                                                             |
| --------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `GitHubClient`        | service tag and interface                                      |                                                                                                                                   |
| `make`                | `(config?: GitHubConfig, env?) => GitHubClient`                | Throws `IntegrationError` `invalid-config` for an `apiBaseUrl` that is not a valid HTTP(S) URL or a `maxRetries` outside 0 to 10. |
| `layer`               | `(config?: GitHubConfig, env?) => Layer<GitHubClient>`         | The client as a layer.                                                                                                            |
| `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 the server asked for, capped at one minute.                                                                              |
| `nextPageUrl`         | `(linkHeader: string \| null) => string \| null`               | The `rel="next"` URL in an RFC 5988 `Link` header.                                                                                |
| `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.                                                                                                 |

`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"`.

### 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.Webhook

GitHub webhook ingress. `X-Hub-Signature-256` is verified over the exact
delivered bytes before anything reads the body. `allowedAssociations` defaults
to `OWNER`, `MEMBER`, and `COLLABORATOR`. Bots, disallowed associations, and
missing associations are refused before routing. Comments and reviews use
their own association, never a parent issue or pull request's association.
Events without an association, including `push`, fail closed.

`SenderPolicy` and `ChannelOptions` accept `allowedAssociations`; an empty
list admits nobody. `defaultAllowedAssociations` exposes the default.
`senderRefusal(event, payload, policy?)` returns `SenderRefused | undefined`.
`SenderRefused` has reason `permission-denied` and a typed `SenderSkipReason`
in `skipReason`: `bot-sender`, `missing-association`, or
`association-not-allowed`. `decode` throws the refusal; the channel converts
it to `InvalidInput` before it can start or signal a run.

| Export           | Signature                                                                                            | Notes                                                                                                                                                                      |
| ---------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verify`         | `(raw: RawInbound, secret: string) => boolean`                                                       | The signature check over `raw.body`, never a re-serialized copy.                                                                                                           |
| `decode`         | `(raw: RawInbound, payload: unknown, receivedAtMs?: number, policy?: SenderPolicy) => ExternalEvent` | Throws an `IntegrationError` `decode-failed` when `X-GitHub-Event` or `X-GitHub-Delivery` is missing. The dedupe key is `<deliveryId>:<eventName>:<correlationId or "*">`. |
| `names`          | `(event: string, payload: unknown) => readonly string[]`                                             | The signal names a delivery answers to, most specific first: the per-action variant ahead of the bare event name.                                                          |
| `correlations`   | `(payload: unknown) => readonly (string \| null)[]`                                                  | `owner/repo#number`, then `owner/repo`, then `null`.                                                                                                                       |
| `idempotencyKey` | `(raw: HasHeaders) => string \| undefined`                                                           | `github:<X-GitHub-Delivery>`, or `undefined` when the header is absent.                                                                                                    |
| `channel`        | `(options: ChannelOptions) => Channel`                                                               | A control-plane channel for GitHub webhooks. The channel name defaults to `github`; fingerprint headers are `x-github-delivery` and `x-github-event`.                      |
| `SERVICE`        | `"github"`                                                                                           | The service segment of every GitHub signal name.                                                                                                                           |

`ChannelOptions` fields: optional `name`, `credential`, `secret`, `route`,
and optional `project`.

### GitHub.ListenerRegistry

Declared GitHub webhooks, and the reconciliation that makes a repository
match the declaration. The safety property is ownership: a hook is owned only
when its numeric GitHub id appears in the workspace's state file. An unowned
hook on a declared callback URL is reported as a `conflict` and never
modified. [The GitHub guide](https://integrations.smithers.sh/guides/github/) walks the workflow.

| Export                      | Signature                                                                   | Notes                                                                                                                                                                                                             |
| --------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reconcile`                 | `(options?: ReconcileOptions) => Effect<ReconcileResult, IntegrationError>` | Plans by default and applies only with `apply: true`; deletes additionally need `allowDelete: true`. An apply that would touch an unowned hook fails `listener-conflict`. An apply holds the workspace lock file. |
| `plan`                      | `(input: PlanInput) => readonly PlanAction[]`                               | Pure: performs no requests and writes nothing.                                                                                                                                                                    |
| `parseRegistry`             | `(input: unknown, source?: string) => Registry`                             | Validates a declaration, reporting every problem it finds. Throws `IntegrationError` `invalid-config`.                                                                                                            |
| `readRegistry`              | `(workspaceRoot: string) => Registry`                                       | Reads `.smithers/listeners.json`. Throws `invalid-config` when the file is missing or invalid.                                                                                                                    |
| `readOwnershipState`        | `(workspaceRoot: string) => OwnershipState`                                 | An empty state when the workspace has never reconciled. A state file that exists but cannot be parsed is fatal.                                                                                                   |
| `parseRemoteHooks`          | `(value: readonly unknown[], repository: string) => readonly RemoteHook[]`  | Decodes the hook list GitHub returned. Throws `decode-failed` naming the member and field.                                                                                                                        |
| `PENDING_CREATE_MAX_AGE_MS` | `86400000`                                                                  | How long a pending create stays adoptable: one day.                                                                                                                                                               |
| `DEFAULT_REGISTRY_PATH`     | `".smithers/listeners.json"`                                                |                                                                                                                                                                                                                   |
| `DEFAULT_STATE_PATH`        | `".smithers/listeners.state.json"`                                          |                                                                                                                                                                                                                   |
| `DEFAULT_LOCK_PATH`         | `".smithers/listeners.lock"`                                                |                                                                                                                                                                                                                   |
| `LISTENER_EVENTS`           | `readonly string[]`                                                         | `issues`, `issue_comment`, `pull_request`, `pull_request_review`, `pull_request_review_comment`.                                                                                                                  |

`ReconcileOptions` fields: `workspaceRoot` (defaults to the ambient working
directory), `registry` (an in-memory declaration instead of the file),
`apply`, `allowDelete`, `token`, `apiBaseUrl`, `env` (replaces the ambient
environment outright), and `client` (an already-built client, which skips the
token check; each listener's `secretEnv` variable is still required).

`reconcile` fails with `credentials-missing` when it has neither a token nor
a client, or when a listener's `secretEnv` variable is unset;
`permission-denied` when listing hooks answers 401, 403, or 404;
`delivery-failed` when a repository's hook list is truncated;
`invalid-config` for workspace file problems; `decode-failed` when GitHub
returns an unreadable hook or no hook id after a create; and
`listener-conflict` when an apply meets an unowned hook on a declared URL or
the lock is held by a live process.

`PlanAction.action` is one of `create`, `update`, `delete`, `noop`, `leave`,
or `conflict`, with `listenerId`, `repository`, `hookId`, `reason`, and
`destructive` alongside. `ReconcileResult` adds `applied` and `skipped` to
the plan summary (`actions`, `changes`, `destructiveChanges`, and the two
paths). Models: `Listener`, `Registry`, `Ownership`, `PendingCreate`,
`OwnershipState`, `RemoteHook`, `PlanInput`.

### 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 action. [Durable actions](https://integrations.smithers.sh/concepts/durable-actions/)
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`. Error schema `IntegrationFailure`.                                                                                                   |
| `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.                                                                                                                                                      |
| `layer`                 | same                                                          | Every GitHub action's implementation in one layer.                                                                                                                                                     |

## Linear

The Linear surface, exported as `Linear` or from `@smthrs/integrations/linear`.

### Linear.Config

`LinearConfig` fields: `apiKey` (a personal API key, sent raw in
`Authorization`; an OAuth token arrives already prefixed; falls back to
`SMITHERS_LINEAR_API_KEY`), `webhookSecret` (falls back to
`SMITHERS_LINEAR_WEBHOOK_SECRET`), and `apiBaseUrl` (falls back to
`SMITHERS_LINEAR_API_BASE_URL`, default `https://api.linear.app/graphql`).

| Export                 | Signature                                               | Notes                                |
| ---------------------- | ------------------------------------------------------- | ------------------------------------ |
| `resolve`              | `(config?: LinearConfig, env?) => ResolvedLinearConfig` | First non-empty value wins, trimmed. |
| `DEFAULT_API_BASE_URL` | `"https://api.linear.app/graphql"`                      | The public GraphQL endpoint.         |

### Linear.LinearClient

The GraphQL client: plain `fetch` over raw GraphQL, with lookup caching, name
resolution, and rate-limit handling. A 429 is retried up to five attempts for
every operation, waiting `Retry-After` or `X-RateLimit-Requests-Reset` capped
at 30 seconds. A 5xx is retried only for a query: on `issueCreate`,
`issueUpdate`, or `commentCreate` the server may have applied the mutation
and lost the answer, so those report `outcomeUnknown` instead of filing a
second issue. Interrupting the fiber aborts the request and the body read.

Service interface:

| Method            | Signature                                                                                                                                        | Notes                                                                                                                                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`           | `(gql: string, variables?: Record<string, unknown>, options?: { retryServerErrors?: boolean }) => Effect<Record<string, any>, IntegrationError>` | A raw GraphQL request resolving with the `data` payload. Fails `credentials-missing` when no API key is configured, `delivery-failed` for transport, HTTP, and GraphQL errors, and `decode-failed` for a non-JSON response. |
| `resolveTeam`     | `(ref: { teamId?: string; teamKey?: string }) => Effect<TeamRef, IntegrationError>`                                                              | Resolves a team by key, case-insensitively, or passes an explicit id through. Cached per client. Supplying both or neither fails `decode-failed`.                                                                           |
| `resolveStateId`  | `(teamId: string, stateName: string) => Effect<string, IntegrationError>`                                                                        | Resolves a workflow-state name such as `In Progress` to its id. Cached per team. An unknown name fails `decode-failed` naming the known states.                                                                             |
| `resolveLabelIds` | `(teamId: string, names: readonly string[]) => Effect<readonly string[], IntegrationError>`                                                      | Resolves label names to ids. Cached per team. Any missing name fails `decode-failed` naming the missing ones.                                                                                                               |
| `getIssue`        | `(idOrIdentifier: string) => Effect<IssueResult, IntegrationError>`                                                                              | Fetches an issue by UUID or by `ENG-123` identifier. A miss fails `decode-failed`.                                                                                                                                          |
| `createIssue`     | `(input: CreateIssueInput) => Effect<IssueResult, IntegrationError>`                                                                             | Files an issue. Exactly one of `teamKey` and `teamId` is required.                                                                                                                                                          |
| `updateIssue`     | `(idOrIdentifier: string, fields: IssueFields) => Effect<IssueResult, IntegrationError>`                                                         | Updates an issue. Resolves the issue first when given an identifier or when a name field needs the issue's team.                                                                                                            |
| `commentOnIssue`  | `(idOrIdentifier: string, body: string) => Effect<CommentResult, IntegrationError>`                                                              | Comments on an issue, resolving an identifier to the UUID first.                                                                                                                                                            |

| Export              | Signature                                                                            | Notes                                                                                                                            |
| ------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `LinearClient`      | service tag and interface                                                            |                                                                                                                                  |
| `make`              | `(config?: LinearConfig, env?) => LinearClient`                                      |                                                                                                                                  |
| `layer`             | `(config?: LinearConfig, env?) => Layer<LinearClient>`                               |                                                                                                                                  |
| `normalizePriority` | `(priority: Priority \| undefined) => number \| undefined`                           | Normalizes a priority name or number onto Linear's 0 to 4 scale. Throws an `IntegrationError` `decode-failed` for anything else. |
| `requirePriority`   | `(priority: Priority \| undefined) => Effect<number \| undefined, IntegrationError>` | The same, in the Effect channel.                                                                                                 |
| `retryDelayMs`      | `(headers: Headers, nowMs?: number) => number \| undefined`                          | The wait the server asked for, capped at 30 seconds.                                                                             |

`IssueFields` accepts `title`, `description`, `priority`, `labels`,
`labelIds`, `stateName`, `stateId`, `assigneeId`, `projectId`, `estimate`,
and `dueDate`. Supply `labels` or `labelIds`, and `stateName` or `stateId`,
not both; both fail `decode-failed`. An empty `labels` array clears the
issue's labels; omitting the field leaves them alone. Name resolution needs
the issue's team. `Priority` is a number 0 to 4 or one of `none`, `urgent`,
`high`, `normal`, `medium`, `low`.

### Linear.Webhook

Linear webhook ingress. Verification checks two things: the
`Linear-Signature` HMAC over the raw body, and the `webhookTimestamp`
freshness window, because a valid signature never expires and a captured
delivery would otherwise be replayable forever.

| Export                      | Signature                                                                                      | Notes                                                                                                                                           |
| --------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `verify`                    | `(raw: RawInbound, secret: string, options?: VerifyOptions) => boolean`                        | Fails closed: a skew that is not a finite integer from 0 to `MAX_TIMESTAMP_SKEW_MS` refuses rather than disabling the replay check.             |
| `decode`                    | `(raw: RawInbound, payload: unknown, source?: string, receivedAtMs?: number) => ExternalEvent` | The dedupe key is `<deliveryId>#<eventName>#<correlationId or "">`.                                                                             |
| `names`                     | `(payload: unknown) => readonly string[]`                                                      | `integration:linear:<type>.<action>`, then `integration:linear:<type>`, lowercased.                                                             |
| `correlations`              | `(payload: unknown) => readonly (string \| null)[]`                                            | The issue identifier (`ENG-123`), the team key (`ENG`), then `null`. A comment delivery carries the issue one level down, under `data.issue`.   |
| `idempotencyKey`            | `(raw: HasHeaders, payload: unknown) => string`                                                | `linear:<deliveryId>`, where the delivery id is the `Linear-Delivery` header or, in its absence, the webhook id, entity, action, and timestamp. |
| `channel`                   | `(options: ChannelOptions) => Channel`                                                         | A control-plane channel for Linear webhooks. The channel name defaults to `linear`; the fingerprint header is `linear-delivery`.                |
| `timestampMs`               | `(value: unknown) => number \| null`                                                           | Reads `webhookTimestamp` as milliseconds; older payloads send seconds.                                                                          |
| `DEFAULT_TIMESTAMP_SKEW_MS` | `60000`                                                                                        | The default freshness window.                                                                                                                   |
| `MAX_TIMESTAMP_SKEW_MS`     | `3600000`                                                                                      | The largest accepted freshness window: one hour.                                                                                                |
| `SERVICE`                   | `"linear"`                                                                                     |                                                                                                                                                 |

`VerifyOptions` fields: `maxTimestampSkewMs` and `nowMs`. `ChannelOptions`
extends them with `name`, `credential`, `secret`, `route`, and `project`.

### Linear.Payload

Schemas for Linear webhook deliveries: `IssueData`, `CommentData`,
`Delivery`, `IssueDelivery`, and `CommentDelivery`. `updatedFrom` carries the
previous values of the fields an `update` changed. Core fields are typed and
everything else passes through.

### Linear.Actions

| Export               | Signature                                                     | Notes                                                                                                                                                                          |
| -------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CreateIssue`        | `Action`, tag `integrations/linear/create-issue`              | Files an issue. Tier `irreversible`. Error schema `IntegrationFailure`.                                                                                                        |
| `CreateIssuePayload` | schema                                                        | `title`, plus optional `teamKey`, `teamId`, `description`, `stateName`, and `labels`. The client resolves the names to ids and enforces exactly one of `teamKey` and `teamId`. |
| `Issue`              | schema                                                        | The issue Linear created: `id`, `identifier`, `title`, `url`.                                                                                                                  |
| `layerCreateIssue`   | `Layer<Requirement<...>, never, LinearClient \| FlowRuntime>` | Implements the action over the client in context.                                                                                                                              |
| `layer`              | same                                                          | Every Linear action's implementation in one layer.                                                                                                                             |

## Telegram

The Telegram surface, exported as `Telegram` or from
`@smthrs/integrations/telegram`.

### Telegram.Config

`TelegramConfig` fields: `botToken` (required; falls back to
`SMITHERS_TELEGRAM_BOT_TOKEN`), optional `apiBaseUrl` (default
`https://api.telegram.org`), `maxRateLimitRetries` (defaults to 3), and
`maxRetryAfterSeconds` (the cap on the server-supplied `retry_after` honored
per retry, defaults to 30).

| Export                 | Signature                                                    | Notes                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolve`              | `(config?: Partial<TelegramConfig>, env?) => TelegramConfig` | Throws `SmithersError` `INVALID_INPUT` when no token can be found. The message names the ways to supply one and never contains token material. |
| `DEFAULT_API_BASE_URL` | `"https://api.telegram.org"`                                 | The public Bot API host.                                                                                                                       |

### Telegram.TelegramClient

The Bot API client: plain `fetch`, no framework. The bot token is redacted
from every error, including one a transport raised with the URL in it. A 429
is retried, waiting the server's capped `retry_after`.

Service interface:

| Method                | Signature                                                                                                                                                                    | Notes                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `call`                | `(method: string, params?: Record<string, unknown>) => Effect<unknown, SmithersError>`                                                                                       | A raw Bot API call returning the `result` field. Retries a 429.                                                                                                                                                                                                                                                                                                                      |
| `sendMessageSmart`    | `(chatId: number \| string, text: string, options?: SendOptions) => Effect<SendResult, SmithersError>`                                                                       | Chunks at 4096 characters, converts markdown to MarkdownV2, and resends a chunk as plain text when Telegram rejects the entities, so a formatting failure costs formatting rather than the message. A failure after earlier chunks landed names their ids in `deliveredMessageIds`. Shows a typing action first unless `typing: false`; a failed typing action never fails the send. |
| `editMessageSmart`    | `(chatId: number \| string, messageId: number, text: string, options?: Pick<SendOptions, "parseMode" \| "inlineKeyboard">) => Effect<unknown, SmithersError>`                | Edits a message, falling back to plain text when Telegram rejects the entities, so fresh content replaces a stale message.                                                                                                                                                                                                                                                           |
| `sendDocument`        | `(chatId: number \| string, document: DocumentInput, options?: { caption?: string; replyToMessageId?: number; messageThreadId?: number }) => Effect<unknown, SmithersError>` | Sends a document by URL or `file_id`, or uploads raw bytes as multipart form data. The caption is converted to MarkdownV2.                                                                                                                                                                                                                                                           |
| `answerCallbackQuery` | `(callbackQueryId: string, options?: { text?: string; showAlert?: boolean }) => Effect<unknown, SmithersError>`                                                              | Answers an inline-keyboard press.                                                                                                                                                                                                                                                                                                                                                    |
| `answerWebAppQuery`   | `(webAppQueryId: string, result: Record<string, unknown>) => Effect<unknown, SmithersError>`                                                                                 | Answers a Mini App inline query: posts `result` to the chat on the user's behalf and closes the Mini App.                                                                                                                                                                                                                                                                            |

`SendOptions` fields: `parseMode` (`markdown` converts standard markdown to
MarkdownV2 and is the default; `MarkdownV2` and `HTML` send the text as-is;
`none` sends raw text with no parse mode; all but `none` keep the plain-text
fallback), `replyToMessageId` (first chunk only), `messageThreadId` (every
chunk), `inlineKeyboard` (last chunk only), `typing` (defaults to true), and
`disableNotification`. `SendResult` fields: `chatId`, `messageIds` (every
chunk's id, in send order), `chunkCount`, and `usedPlainTextFallback`.
`DocumentInput` is a string (URL or `file_id`) or `{ filename, content,
contentType? }`.

| Export               | Signature                                                           | Notes                                                                                                                                                                                                                                                                                                                                               |
| -------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TelegramClient`     | service tag and interface                                           |                                                                                                                                                                                                                                                                                                                                                     |
| `make`               | `(config?: Partial<TelegramConfig>, env?) => TelegramClient`        | Throws `SmithersError` `INVALID_INPUT` when no token can be found.                                                                                                                                                                                                                                                                                  |
| `layer`              | `(config?: Partial<TelegramConfig>, env?) => Layer<TelegramClient>` |                                                                                                                                                                                                                                                                                                                                                     |
| `TelegramApiError`   | class, code `TELEGRAM_API_ERROR`                                    | Carries the Bot API's `errorCode`, `retryAfterSeconds`, `deliveredMessageIds`, and an optional `reason` override for a failure the transport did not report. The token is never part of any field.                                                                                                                                                  |
| `isTelegramApiError` | `(error: unknown) => error is TelegramApiError`                     | Guarded against forged names and throwing getters.                                                                                                                                                                                                                                                                                                  |
| `toIntegrationError` | `(error: unknown) => unknown`                                       | Maps a `TelegramApiError` onto `IntegrationError`: a 429 or 5xx becomes a retryable `delivery-failed` with `outcomeUnknown` for a 5xx or a lost connection; a 401 or 403 becomes `permission-denied`; a 400 or 404 becomes `decode-failed`. Anything else passes through unchanged. The action boundary applies this before `fromIntegrationError`. |
| `redactBotToken`     | `(text: string, botToken: string) => string`                        | Removes the literal token and any `/bot<id>:<secret>` path segment from a string.                                                                                                                                                                                                                                                                   |

### Telegram.Chunk

Message chunking at Telegram's hard `sendMessage` limit.

| Export               | Signature                                                 | Notes                                                                                                                                                                                                                                                                                                                                                                      |
| -------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chunk`              | `(text: string, maxLength?: number) => readonly string[]` | Splits at the last paragraph break, line break, sentence end, or word boundary that fits, and cuts mid-word only for a single unbroken run longer than the limit. A cut never lands inside a UTF-16 surrogate pair. Chunks are trimmed of the whitespace they were split on. Throws `SmithersError` `INVALID_INPUT` when `maxLength` is not an integer between 1 and 4096. |
| `MAX_MESSAGE_LENGTH` | `4096`                                                    | Telegram's maximum `sendMessage` text length.                                                                                                                                                                                                                                                                                                                              |

### Telegram.Markdown

Standard markdown to Telegram MarkdownV2.

| Export       | Signature                           | Notes                                                                                                                                                                                                                                                             |
| ------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toTelegram` | `(markdown: string) => string`      | Handles fenced and inline code, links, bold (`**` to `*`), strikethrough (`~~` to `~`), italic, and headings, which become bold because an unescaped `#` is one of the characters Telegram rejects. Everything outside a recognized token is escaped defensively. |
| `escape`     | `(text: string) => string`          | Escapes plain text for MarkdownV2.                                                                                                                                                                                                                                |
| `clean`      | `(text?: string \| null) => string` | Strips NUL characters, which collide with the conversion's sentinel scheme and which Telegram rejects anyway.                                                                                                                                                     |

### Telegram.Source

The `getUpdates` long-poll source. Confirming an offset is what tells
Telegram to forget those updates, so the cursor is committed after the
handler has processed the batch.
[Events, signals, and cursors](https://integrations.smithers.sh/concepts/events-and-signals/) explains the
contract.

`Options` fields (only `allowedChatIds` is required): `sourceId` (the cursor key and dedupe scope,
defaults to `telegram`), `pollTimeoutSeconds` (how long the Bot API holds a
poll open, defaults to 25), `allowedUpdates` (defaults to `message`,
`edited_message`, and `callback_query`), `allowedChatIds` (required and non-empty; missing or empty throws
`invalid-config` before polling; updates from other
chats are dropped, and so is an update whose chat the source cannot
determine; the offset still advances past every dropped update once the rest
of the batch is handled), `client` (an already-built client), and the
`TelegramConfig` fields.

Service interface:

| Method | Signature                                                                                                                                | Notes                                                                                                                                                                                                                                                                                                                                                                                                |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `poll` | `(cursor: string \| null) => Effect<Batch, IntegrationError>`                                                                            | One poll turn against the stored offset. Commits nothing. A stored cursor that is not a decimal offset fails `invalid-config` rather than replaying the backlog. A failed `getUpdates` fails `poll-failed`. A result that is not an update array, an update with no numeric `update_id`, or an update whose `message`, `edited_message`, or `callback_query` is not an object fails `decode-failed`. |
| `run`  | `(onBatch: (events) => Effect<void, E, R>, options?: { schedule?: Schedule }) => Effect<never, IntegrationError \| E, R \| CursorStore>` | Polls forever: read the cursor, poll, hand the batch to `onBatch`, and commit the offset only after `onBatch` succeeds. Repeats on the given schedule, defaulting to one turn every 250 milliseconds. Requires `CursorStore`.                                                                                                                                                                        |

`Batch` fields: `events`, and `cursor`, which is absent when the poll
returned nothing.

| Export                 | Signature                                                                                         | Notes                                                                                                                                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Source`               | interface                                                                                         | `sourceId`, `poll`, `run`.                                                                                                                                                                                                                |
| `make`                 | `(options: Options, env?) => Source`                                                              | Throws an `IntegrationError` `invalid-config` for a source id that is empty or padded with whitespace, and `SmithersError` `INVALID_INPUT` when no bot token can be found.                                                                |
| `updateToEvents`       | `(source: string, update: Record<string, any>, receivedAtMs: number) => readonly ExternalEvent[]` | Maps one update onto its events. A message carrying `message_thread_id` emits a chat-scoped and a thread-scoped event with distinct dedupe keys; one carrying `web_app_data` additionally emits a separately deduped Mini App data event. |
| `idempotencyKey`       | `(event: ExternalEvent) => string`                                                                | The event's dedupe key, already scoped to the source.                                                                                                                                                                                     |
| `chatCorrelationId`    | `(chatId: number \| string) => string`                                                            | `chat:<id>`.                                                                                                                                                                                                                              |
| `threadCorrelationId`  | `(chatId: number \| string, threadId: number \| string) => string`                                | `chat:<id>:thread:<id>`.                                                                                                                                                                                                                  |
| `MESSAGE_EVENT`        | `"integration:telegram:message"`                                                                  |                                                                                                                                                                                                                                           |
| `EDITED_MESSAGE_EVENT` | `"integration:telegram:edited_message"`                                                           |                                                                                                                                                                                                                                           |
| `CALLBACK_QUERY_EVENT` | `"integration:telegram:callback_query"`                                                           |                                                                                                                                                                                                                                           |
| `WEB_APP_DATA_EVENT`   | `"integration:telegram:web_app_data"`                                                             |                                                                                                                                                                                                                                           |
| `SERVICE`              | `"telegram"`                                                                                      |                                                                                                                                                                                                                                           |

### Telegram.Approval

Inline-keyboard approvals. Telegram caps `callback_data` at 64 bytes, so a
press carries a compact code and nothing else. It also carries no trust: any
member of the chat can press a button, so a caller that cares re-authorizes
on the presser's user id. The per-approval token is a 32-bit namespace, not a
secret, and it is what keeps one prompt's buttons from resolving another's:
a press whose token does not match fails safe, and a prompt built with no
token matches nothing at all.

| Export                    | Signature                                                                                    | Notes                                                                                                                                                                                                                                                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`                   | `(id: string) => string`                                                                     | A short, colon-free namespace derived from an id. Throws `SmithersError` `INVALID_INPUT` for an id that is not a non-empty string.                                                                                                                                                                                   |
| `callbackData`            | `(choice: Choice, approvalToken: string) => string`                                          | Encodes a choice as `sap:<token>:a`, `sap:<token>:d`, or `sap:<token>:s:<key>`. Throws `INVALID_INPUT` for a token that is not a string, a token containing `:`, an empty or colon-bearing option key, or data over the 64-byte limit.                                                                               |
| `parseCallbackData`       | `(data?: string \| null) => (Choice & { token: string }) \| null`                            | Decodes `callback_data`, or `null` when the press is not one of ours. Reads only the exact grammar `callbackData` emits.                                                                                                                                                                                             |
| `isOwnPress`              | `(callbackQuery: { data?: string }, spec: KeyboardSpec) => boolean`                          | Whether a delivered callback query is a press on this approval's buttons.                                                                                                                                                                                                                                            |
| `keyboard`                | `(spec: KeyboardSpec) => InlineKeyboard`                                                     | Builds the keyboard for a prompt. Throws `INVALID_INPUT` for `select` mode with no options.                                                                                                                                                                                                                          |
| `webAppButton`            | `(text: string, url: string) => InlineKeyboardButton`                                        | A Mini App button. Throws `INVALID_INPUT` unless the URL is HTTPS, which is Telegram's own rule.                                                                                                                                                                                                                     |
| `approverLabel`           | `(callbackQuery: { from?: { id?: number \| string; username?: string } }) => string \| null` | Who pressed the button, as `@username` or the numeric id.                                                                                                                                                                                                                                                            |
| `decision`                | `(callbackQuery, spec: KeyboardSpec, nowMs?: number) => Decision \| Selection`               | Maps a delivered callback query to a decision. A press that is not this approval's own fails safe: a rejection in `approve` mode, an empty selection in `select` mode, which accepts only a key this approval offered. `decidedAt` is the resolution wall clock; Telegram does not report when a button was pressed. |
| `CALLBACK_DATA_MAX_BYTES` | `64`                                                                                         | Telegram's hard limit on `callback_data`.                                                                                                                                                                                                                                                                            |

Models: `Choice` (`approve`, `reject`, or `select` with a `key`), `Option`
(`key` and `label`), `KeyboardSpec` (`mode`, optional `token`, `options`,
`allowedChatIds`, `approveText`, `rejectText`, `miniAppUrl`, `miniAppText`), `Decision`
(`approved`, `note`, `decidedBy`, `decidedAt`), and `Selection` (`selected`,
`notes`).

`decision` accepts approve and selection presses only when `from.id` is in
`spec.allowedChatIds`, compared as strings. Pass the source's allowlist here
as well. Missing or empty lists authorize nobody. A group chat id does not
authorize that group's members; include approvers' individual user ids.
`isOwnPress` checks only the prompt token, not sender authorization.

### Telegram.InitData

Telegram Mini App `initData` verification, on Web Crypto with no `node:`
builtin. Two paths: HMAC when you hold the bot token, and Ed25519 for a third
party holding only the numeric bot id. Node is the only runtime these two
paths are verified on; read any other as untested.

| Export                    | Signature                                                                                            | Notes                                                                                                                                                                                                                                                                    |
| ------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `parse`                   | `(initData: string) => InitData`                                                                     | Parses the query string into its fields without verifying it. Verification is what makes the fields trustworthy.                                                                                                                                                         |
| `verifyWithBotToken`      | `(initData: string, botToken: string, options?: VerifyOptions) => Promise<InitData>`                 | The HMAC path. Resolves with the parsed fields. Rejects with `TELEGRAM_INIT_DATA_INVALID` for a bad or stale payload, `INVALID_INPUT` for a missing token or bad options, and `UNSUPPORTED` when the runtime has no Web Crypto. The token never appears in error output. |
| `verifySignature`         | `(initData: string, botId: number \| string, options?: VerifySignatureOptions) => Promise<InitData>` | The Ed25519 path, for a third party that must authenticate a Mini App user without holding the bot token. Same rejection codes, with `UNSUPPORTED` when the runtime lacks Ed25519.                                                                                       |
| `ED25519_PUBLIC_KEY_PROD` | hex string                                                                                           | Telegram's production Ed25519 public key, the default for `verifySignature`.                                                                                                                                                                                             |
| `ED25519_PUBLIC_KEY_TEST` | hex string                                                                                           | Telegram's test-datacenter key.                                                                                                                                                                                                                                          |

`VerifyOptions` fields: `maxAgeSeconds` (rejects `initData` older than this;
defaults to 3600, `0` disables the age check, and anything outside 0 to
86400 is a configuration error) and `nowMs`. Both ends of the freshness
window are bounded: a correctly signed `auth_date` dated far ahead is
refused. `VerifySignatureOptions` adds `publicKeyHex`, exactly 64 hexadecimal
characters. `InitData` fields include `raw`, `hash`, `signature`, `authDate`,
`queryId`, `user`, `receiver`, `chat`, `chatType`, `chatInstance`,
`startParam`, and `params` (every decoded pair).

### Telegram.Payload

Schemas for the Bot API objects this package delivers as payloads: `Chat`,
`User`, `Message` (delivered for `message` and `edited_message` events),
`CallbackQuery`, `WebAppData`, and `WebAppDataMessage`. Core fields are typed
and everything else passes through.

### Telegram.Actions

| Export               | Signature                                                       | Notes                                                                                                                                                                                                                          |
| -------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `SendMessage`        | `Action`, tag `integrations/telegram/send-message`              | Sends a message to a chat. Tier `irreversible`. Error schema `IntegrationFailure`. Not atomic: text over the limit is several `sendMessage` calls inside the step, and a partway failure journals the ids already delivered.   |
| `SendMessagePayload` | schema                                                          | `chatId` (a string, because Telegram uses both numeric ids and `@channel` usernames, and a numeric id exceeds the range JSON round-trips exactly), `text`, and optional `parseMode`, `messageThreadId`, `disableNotification`. |
| `Sent`               | schema                                                          | `chatId`, `messageIds`, `chunkCount`, `usedPlainTextFallback`.                                                                                                                                                                 |
| `layerSendMessage`   | `Layer<Requirement<...>, never, TelegramClient \| FlowRuntime>` | Implements the action over the client in context, mapping `TelegramApiError` through `toIntegrationError` before `fromIntegrationError`.                                                                                       |
| `layer`              | same                                                            | Every Telegram action's implementation in one layer.                                                                                                                                                                           |

---

# @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://jj.smithers.sh)

> **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` and `revert` are optional on the type, so
a hand-written test double may leave them out.

| Member                                | Signature                                                                                       |
| ------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `snapshot(message?)`                  | `(message?: string) => Effect<{ readonly changeId: ChangeId }, JjFailure>`                      |
| `restore(changeId)`                   | `(changeId: ChangeId) => Effect<void, JjFailure>`                                               |
| `diff(from, to)`                      | `(from: ChangeId, to: ChangeId) => Effect<string, JjFailure>`                                   |
| `workspaceAdd(name, path, revision?)` | `(name: string, path: string, revision?: ChangeId) => 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(changeId)` (optional)         | `(changeId: ChangeId) => Effect<{ readonly reverted: ReadonlyArray<string> }, JjFailure>`       |

`snapshot` commits the working copy and returns the change id to restore to
later: it closes the current change and opens a fresh one. Node and Bun only
label a closed change that has no existing description.
With no message there is no `describe` at all, because `jj describe` without
`-m` starts `$JJ_EDITOR` and waits for it.

`restore` puts the working copy back to `changeId`, 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, pinned at `revision` when one is given. `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".

`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 both 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.

Both optional operations are capability-checked like every other one:
`jj:root` is `sealed` and `jj:revert` is `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`                | The durable handle a run uses to name workspace state. A bare alias, because the value jj prints is the value stored. |

### Failures

| Export                | Signature                                                                                                                          | Meaning                                                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `JjErrorCode`         | `Schema.Literals<["not_installed", "conflict", "invalid_ref", "snapshot_refused", "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, JjError>`                                                  | Its own child     | The process cwd |
| `layerAt(repositoryRoot)`        | `(repositoryRoot: string) => Layer<Jj, JjError>`                      | Its own child     | Bound, absolute |
| `layerSpawner`                   | `Layer<Jj, JjError, ChildProcessSpawner>`                             | The host spawner  | The process cwd |
| `layerSpawnerAt(repositoryRoot)` | `(repositoryRoot: string) => Layer<Jj, JjError, 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(message)` first close the current change, then describe
that closed change only if it was unnamed. Existing operator descriptions are
preserved; the fresh working copy remains unnamed. Without a message, no
`describe` or editor runs. The browser's frozen WASM ABI still replaces the
closed change's description when a message is supplied.

Repository state operations are fenced as a unit. `snapshot`, `restore`, and
`diff` share one single-permit semaphore per repository inside a process and an
exclusive `.jj/smithers.lock` owner directory across processes. The snapshot's
CLI calls therefore cannot interleave with another state operation. 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.

Snapshot messages are opaque strings on both browser and CLI layers, including
empty strings, leading `-`, quotes, and newlines. Node and Bun pass messages as
`-m=<message>`; 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. Before exposing `Jj`, all CLI layers await a
local `jj --version` probe through `node:child_process`, outside the host's
process spawner and crash-reaping journal. The probe does not use or require the
repository directory: bound layers can be built before runtime storage creates
that directory. Repository operations still require a valid working directory.
Concurrent and subsequent layer builds share the probe result per resolved
executable path for the lifetime of the process. Restart the process after replacing a binary at the same path.
Repository commands still use the layer's chosen process runner. An older or
unrecognized version fails construction with `JjError.code = "unsupported_version"`
and the required minimum; a missing binary fails construction with `not_installed`.
All four CLI layers therefore have `JjError` in their layer 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.

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()` rather than passing silently. A
resolution that came from `PATH` is spawned as the bare name `jj`, so a host
spawner that hands the child a different `PATH` still decides for itself.
`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
every caller's soft-failure behavior while giving `doctor` something specific
to print. 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>` |
| `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.

`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 browser backend answers differently from the CLI, each
because the frozen wasm ABI has no operation or field for it:

- `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 CLI adapter's single command needs no
  rollback.
- `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 symlink degradation,
is in [Run jj in a browser tab](https://jj.smithers.sh/guides/run-jj-in-a-browser/#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`       | `class WasiExitError extends Error` with `readonly exitCode: number`                                                       |
| `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` are no-ops, because a synchronous slice is durable
  the moment each call returns.
- `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` re-lists the directory on each call and uses the entry index as
  the cookie, so a directory mutated between two reads of one iteration can skip
  or repeat a name. That is unobservable from the single-threaded module this
  shim hosts.
- 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`, `truncateSync`. |
| `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.

There is deliberately no `fsyncSync`: a synchronous backend is durable the
moment a call returns. `ftruncateSync` 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 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://journal.smithers.sh)

> **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                |
| `transact`            | `<A, E, R>(effect: Effect<A, E, R>) => Effect<A, E \| JournalError, R>`                            | runs a state projection and its `emitDurable` calls in one transaction            |
| `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://journal.smithers.sh/guides/commit-state-and-entry/).

`owner` is mandatory on `emitDurable`, `checkpoint`, and `compact`. The insert,
the checkpoint replacement, and the truncation land only while `flows_runs`
still records that owner as the running run's owner, 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.

`flows_runs` belongs to [`@smthrs/run-store`](/docs/reference/api/run-store/), so a composition
that installs only this package's migrations fails every fenced call with
`sink_failed` and `no such table: flows_runs`.

### 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://journal.smithers.sh/troubleshooting/).

### 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?, limit }`, `limit` in `1..maxEntriesLimit`                                              |
| `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                                                                                   |

`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.

`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`             | unset              | entries the queued writer commits per transaction                             |
| `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://journal.smithers.sh/concepts/idempotency/).

## 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.

## 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? }`                      | one textual rule                                                        |
| `Options`          | `{ rules?, onTooDeep? }`                         | `onTooDeep` is `"throw"` (default) or `"name"`                          |
| `defaultRules`     | `ReadonlyArray<Rule>`                            | the built-in credential shapes                                          |
| `placeholder`      | `"[REDACTED]"`                                   | the substitution                                                        |
| `maxDepth`         | `256`                                            | container edges traversed before a payload is refused                   |
| `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 with optional whitespace
around `:` or `=`, Basic and Bearer authorization, bare JWTs, private-key PEM
blocks (including OpenSSH), and cookie/session assignments. Structured
credential suffixes also include `secretAccessKey`, `passphrase`, `sshKey`,
`session`, `signature`, and `auth`. See the full
[redaction rules](https://journal.smithers.sh/concepts/redaction/).

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://journal.smithers.sh/concepts/redaction/).

## 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://journal.smithers.sh/guides/redact-log-output/).

## 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://journal.smithers.sh/guides/fold-a-projection/).

## 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"`           |

`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.

No exporter ships in this package. Provide one, for example
[`@smthrs/observability`](/docs/reference/api/observability/), and these counters appear in it.

## 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` | `{ capacity?, overflow?, batchSize?, sourceEventCache?, redact?, compaction? }` |

Every field forwards to `SqlJournal.layer` unchanged. The defaults are
`capacity: 1024` and `overflow: "reject"`. This bundle creates the journal's
tables only, so a suite exercising a fenced call supplies `flows_runs` itself
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://journal.smithers.sh/guides/testing/).

## 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://journal.smithers.sh/guides/commit-state-and-entry/#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://journal.smithers.sh/concepts/compaction/) 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://journal.smithers.sh/concepts/state-event-authority/): 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.

---

# @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://kernel.smithers.sh)

> **Related APIs:** [`@smthrs/capability`](/docs/reference/api/capability/) · [`@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.
:::

## 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 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. `reply` and `grantEnvelope` are uninterruptible
and serialized against `check`.

### 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.

### 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 maximumIdentityLength = 4_096
const maximumCapabilityResourceLength: number // Capability.maxResourceLength
```

Every bound failure uses `invalid_resolution` and occurs before state or
journal authority changes.

## 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, AtomicRequest, AtomicFileSystem, AtomicHostFileSystem

```ts
const AtomicFileSystemTypeId: unique symbol

interface AtomicRequest {
  readonly operation: string
  readonly boundaryRoot?: string | undefined
  readonly logicalRoot?: string | undefined
  readonly path?: string | undefined
  readonly from?: string | undefined
  readonly to?: string | undefined
  readonly pattern?: string | undefined
  readonly root?: string | undefined
  readonly data?: string | undefined
  readonly encoding?: string | undefined
  readonly options?: object | undefined
}

interface AtomicFileSystem {
  readonly execute: <A>(request: AtomicRequest) => Effect.Effect<A, 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.

### 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.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/unstable/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.layer

```ts
const layer: Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | GrantStore>
```

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
capability resource is `CommandLine.render(command)` alone: the working
directory, environment overrides, and pipeline `from`/`to` routing are not part
of what a grant authorizes. `cwd` and the **names** of overridden environment
variables reach an attended surface as display metadata; the values do not. A
command that cannot be snapshotted fails with an `InvalidData` `PlatformError`.

## 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`.                                      |
| `isCredentialName`      | `(name: string) => boolean`                      | Tests one name against that rule.                                                               |
| `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. 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.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/unstable/http/HttpClient"
```

Effect's tag and constructor, unchanged.

### 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. Requests are snapshotted before any
suspension. Redirects are followed **above** the guard, and platform clients do
not follow redirects on their own, so every hop re-enters authorization
independently.

## 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. `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>
```

Effect's path service, re-provided transparently. 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.

## 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`. Add
`"@smthrs/testing": "workspace:*"` to the consuming package's devDependencies;
see [Testing](https://kernel.smithers.sh/testing/).

### @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://kernel.smithers.sh/guides/adapt-a-new-host-platform/).

## 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. It does not sandbox the operating system and cannot observe host access that bypasses the decorated services. Hermetic 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/).

---

# @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://keys.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/)

`@smthrs/keys` exports one module. Every name below is available from the root
entry point and from `@smthrs/keys/Key`.

```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://keys.smithers.sh/concepts/key-derivation/).

### 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://keys.smithers.sh/concepts/key-material/).
- 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>
```

The same derivation as a schema transformation, for composing inside a decode.

- 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 no schema issue
  retains the input value even when an enclosing caller requests input
  reporting.
- 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://keys.smithers.sh/guides/derive-a-key-inside-a-schema/).

## 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, cause: Schema.Unknown }
) {}
```

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 for diagnostics. A
  canonicalization cause names the JSON path of the offending value, which
  includes object property names. See
  [Handle a derivation failure](https://keys.smithers.sh/guides/handle-a-derivation-failure/).

### 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://keys.smithers.sh/guides/separate-identity-namespaces/).

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://keys.smithers.sh/concepts/wire-format/).

---

# @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://mcp.smithers.sh)

> **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 transport 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/).

## McpClient

A minimal MCP client covering the `initialize` handshake, `tools/list`, and
`tools/call` over stdio. It is deliberately not a general MCP SDK: resources,
prompts, sampling, and roots are not wired up.

### McpClient.connect

```ts
const connect: (
  options: ConnectOptions
) => Effect.Effect<McpClient, McpError, ChildProcessSpawner | Scope.Scope>
```

Spawns the server, completes the handshake, and fetches its tool catalog once,
up front, following `nextCursor` across pages.

Requires `ChildProcessSpawner` and a `Scope`. The connection's lifetime is the
scope's lifetime: closing the scope tears the process down, and every request
pending at that moment fails with `connection_closed`.

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://mcp.smithers.sh/guides/bound-an-untrusted-server/).

### 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, never containing `/` or a 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

| 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.                                                          |
| `maxCatalogPages`       | `number \| undefined`                              | Maximum `tools/list` pages walked. Default 32.                                                            |

Every numeric field must be a positive safe integer. Anything else fails with
`protocol_error` naming the option, before the process is spawned.

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 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.clientInfo

```ts
const clientInfo: { readonly name: string; readonly version: string }
```

The frozen identity disclosed to every server during initialization:
`{ name: "smithers", version: "1.0.0-rc.0" }`. 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`      |
| `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://mcp.smithers.sh/guides/handle-a-failed-tool-call/).

## 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`, `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:**",
  "model:call:**",
  "proc:spawn:**",
  "jj:status:**",
  "jj:diff:**",
  "jj:snapshot:**",
  "jj:restore:**",
  "jj:workspace-add:**",
  "jj:workspace-forget:**",
  "jj:root:**",
  "jj:revert:**"
]
```

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://mcp.smithers.sh/guides/grant-authority-to-mcp-tools/).

### 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://memory.smithers.sh)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@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, and `Flows.handlers` is that pair for the bare declarations the module exports. `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                                       |
| ----------- | -------------------------- | --------------------------------------------- |
| `namespace` | a `Namespace`              | where memory this tree reads and writes lives |
| `recall`    | `"auto"`, `"none"`         | whether recall runs at all                    |
| `maxTokens` | integer                    | the budget recall answers within              |
| `retain`    | `"on-complete"`, `"never"` | whether writes are kept                       |

`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 the policy bank. Every explicit bank must resolve to the policy namespace, 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 policy namespace. 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`. 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, {
  namespace: { kind: "flow", id: "release-notes" },
  recall: "auto",
  maxTokens: 2048,
  retain: "on-complete"
})

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` declares the topology a model-authored plan fits inside, and fills its leaf slots at run time, so a leaf cannot be handed a namespace at declaration time. `MemoryTrellis.make` applies one policy to the author, to the leaf, and to the memory flows those declare, then annotates the trellis itself.

It takes everything `Trellis.make` takes, plus `memory`. 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: {
    namespace: { kind: "flow", id: "release-notes" },
    recall: "auto",
    maxTokens: 2048,
    retain: "on-complete"
  }
})
```

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 drives the plan with `Trellis.run` 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 text.
- `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` 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         |

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` optionally restricts the read to at most 64 exact `{ kind, id }` identities, while preserving namespace, TTL, status, supersession, and tag filtering.

`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.

## 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`  | a retry reused an id with different immutable creation data       |
| `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://memory.smithers.sh/troubleshooting/) 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://memory.smithers.sh/surface/) 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`                                             | Every call fails with `embedding_unavailable`: no embedding provider is configured.                                                                                 |
| `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 embedding: 64 dimensions, NFKC-normalized input.                                                                                |
| `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`.                                                                                                               |
| `RememberInput`       | schema                                                                                       | `{ bank, key, text, tags?, ttlMs? }`. Tags use `Namespace.Tags`, so model decoding and durable writes enforce the same vocabulary, uniqueness rule, and 16-tag cap. |
| `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.                                                                                       |
| `recallSlot`          | `Pattern` slot                                                                               | `Recall.slot`, the flow-valued slot shared by the keyword, FTS, and semantic bindings.                                                                              |
| `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.                                                          |
| `handlers`            | `Handlers`                                                                                   | The handlers for the bare declarations this module exports. They carry no policy, so they behave as unscoped memory.                                                |
| `RememberInputType`   | type                                                                                         | What the `remember` flow accepts.                                                                                                                                   |
| `RecallInputType`     | type                                                                                         | What the `recall` flow accepts.                                                                                                                                     |
| `RecallOutputType`    | type                                                                                         | What the `recall` flow returns.                                                                                                                                     |

### `@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.                                                                               |
| `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?, makeSummaryId? }`; `keepRecent` defaults to 2.                                                                                               |
| `CompactionResult`    | interface                                                                   | `{ compactedThreads: number, deletedMessages: number }`.                                                                                                                            |
| `compact`             | `(options) => Effect<CompactionResult, E \| MemoryError, R \| MemoryStore>` | Summarizes old history and atomically replaces it with one summary message. The summarizer runs before the write transaction; interruption before the commit leaves sources intact. |

### `@smthrs/memory/MemoryError`

| Export            | Signature                                            | Behavior                                                                                                    |
| ----------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `MemoryErrorCode` | schema and type                                      | The ten 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.                                                                       |
| `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?, tagGroup?, 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, deleteIds }`.                                                                                          |

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.                                                                                                                           |
| `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.                                                                                                             |
| `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`.                        |

| Export      | Signature                                                                      | Behavior                                                                              |
| ----------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `make`      | `Effect<Service, MemoryError, Crypto.Crypto \| DurableWriter \| SqlClient>`    | Builds the SQL-backed service and applies the package's idempotent migrations.        |
| `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               | Everything `Trellis.make` accepts plus `memory`, the policy the authored plan runs under.                   |
| `Parts`       | interface               | `{ author, leaf }`: the scoped flows to hold when you drive the plan yourself with `Trellis.run`.           |
| `parts`       | `(options) => Parts`    | Applies the policy to the author and the leaf without composing them.                                       |
| `make`        | `(options) => Flow.Any` | 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](https://smithers.sh/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/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.                                                                                  |
| `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.      |
| `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. |
| `make`                        | `(implementation: Service) => Service`       | Constructs a recall service.                                                                                          |
| `layer`                       | `(implementation: Service) => Layer<Recall>` | Provides a recall service.                                                                                            |
| `makeNoop`                    | `() => Service`                              | Answers no rows.                                                                                                      |
| `layerNoop`                   | `Layer<Recall>`                              | Provides the empty implementation.                                                                                    |
| `NamespaceValue`              | type                                         | `Namespace.Namespace`, for associating a bank with a structured namespace.                                            |
| `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`                 | interface                                                    | A row accepted from the store retrieval seam: `{ bank?, namespace?, key, text, tags, status?, updatedAtMs }`. |
| `recall`              | `(input: Input) => Effect<Output, MemoryError, MemoryStore>` | Runs keyword recall against the supplied store.                                                               |
| `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, key, model, contentDigest, dimensions, vector, updatedAtMs, recordKind?, recordId? }`; components may be a readonly array or `Float32Array`. |
| `VectorStore`        | interface                                                                         | The injectable vector-table adapter: `upsert(vector)` and finite `scan(banks, model)` pages of at most 64 vectors.                                                              |
| `Options`            | interface                                                                         | `{ vectorStore, model?, halfLifeMs? }`.                                                                                                                                         |
| `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.                                                                                                                   |
| `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.                                                                                                                    |
| `Projector`          | interface                                                                         | `{ project(row), activeKeys() }`: the per-key serialized projection coordinator. There is no callable projector alias.                                                          |
| `makeProjector`      | `(options: Options) => Projector`                                                 | Constructs the coordinator. Projection retries once and logs failures without changing the write result.                                                                        |
| `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.                                                                                           |
| `cosineSimilarity`   | `(left, right) => number`                                                         | Cosine similarity between two embedding vectors.                                                                                                                                |
| `recencyDecay`       | `(updatedAtMs, nowMs, halfLifeMs) => number`                                      | The exponential recency weight, with a default half-life of seven days inside `recall`.                                                                                         |

### `@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<string, never, MemoryStore \| Recall> }`.                                                                                            |
| `DeclaredText` | interface                                                                              | `{ text, digest }`: the exact shape `Agent.Options.memory` accepts.                                                                                         |
| `make`         | `(options?: { capacity?: number }) => Source`                                          | Constructs a memoizing source. `capacity` defaults to 1,024 identities and must be a positive safe integer.                                                 |
| `source`       | `Source`                                                                               | The default source value.                                                                                                                                   |
| `declaredText` | `(source: Source, input: Input) => Effect<DeclaredText, never, MemoryStore \| Recall>` | Reads the frozen snapshot and digests it. Fetches once per `(lineageId, iteration)` and degrades to empty text after a two-second timeout or typed failure. |
| `byteLength`   | `(text: string) => number`                                                             | The UTF-8 byte length every memory budget is stated in.                                                                                                     |
| `truncate`     | `(text: string, maxBytes: number) => string`                                           | Truncates to a byte budget without splitting a code point.                                                                                                  |

### `@smthrs/memory/SnapshotRecorder`

| Export             | Signature                                              | Behavior                                                                                             |
| ------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `Identity`         | interface                                              | `{ lineageId, iteration }`: the stable identity of one opening snapshot.                             |
| `Service`          | interface                                              | `{ record(identity, effect) }`: answer the recorded value, 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.        |
| `make`             | `(implementation: Service) => Service`                 | Builds a recorder.                                                                                   |
| `layer`            | `(implementation: Service) => Layer<SnapshotRecorder>` | Provides a recorder.                                                                                 |

### `@smthrs/memory/WithMemory`

| Export         | Signature                                                                                                 | Behavior                                                                                                                                       |
| -------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `Policy`       | schema and type                                                                                           | `{ namespace, recall: "auto" \| "none", maxTokens, retain: "on-complete" \| "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.                                                                                              |
| `references`   | `(flow: Flow.Any) => ReadonlyArray<Flow.Reference>`                                                       | The collaborators a dynamic flow declares, callable flows and unresolved registry names alike.                                                 |
| `children`     | `(flow: Flow.Any) => ReadonlyArray<Flow.Any>`                                                             | The callable flows a dynamic flow declares.                                                                                                    |
| `policyOf`     | `(flow: Flow.Any) => 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` | 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://memory.smithers.sh/installation/) and [Import surface](https://memory.smithers.sh/surface/).

---

# @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://migrate.smithers.sh)

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.

## 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://migrate.smithers.sh/guides/embed-the-scanners/) 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`                               | `WithApproval.withApproval(inner, { reason, approval })`                                     | `@smthrs/patterns/WithApproval`                                                 | automatic |
| `ApprovalGate`                           | `WithApproval.withApproval`                                                                  | `@smthrs/patterns/WithApproval`                                                 | 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`                          | none                                                                                         | none                                                                            | unsafe    |
| `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`                        | none                                                                                         | none                                                                            | unsafe    |
| `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`                          | none                                                                                         | none                                                                            | unsafe    |
| `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`                                 | none                                                                                         | none                                                                            | unsafe    |
| `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`                          | none                                                                                         | none                                                                            | unsafe    |
| `MergeQueue`                             | none                                                                                         | none                                                                            | unsafe    |
| `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`                              | none                                                                                         | none                                                                            | unsafe    |
| `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`                                | `Node.bindPlanned with WithApproval`                                                         | `@smthrs/plan/Node, @smthrs/patterns/WithApproval`                              | 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`                                | none                                                                                         | none                                                                            | unsafe    |
| `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/core/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`                             | none                                                                                         | none                                                                            | unsafe    |
| `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`                                | none                                                                                         | none                                                                            | unsafe    |
| `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, { payload, success, error, body }) plus the default descriptor`              | `@smthrs/flow/Flow, @smthrs/core/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`                          | none                                                                                         | none                                                                            | unsafe    |
| `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 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`                        | `@smthrs/scorers, @smthrs/evals`                                                             | `@smthrs/scorers, @smthrs/evals`                                                | 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 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 1.0.0-rc.0 and effect at 4.0.0-rc.112`                                         | 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/core/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 exactly `4.0.0-rc.112`
  raises `effect-pin-conflict` naming it: a range, a later prerelease, and two
  manifests that disagree are each a version this release was not built
  against. `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, so the later
modules parse nothing twice.

## 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, and `Effect` too when a field has a
default.

`ZodSchemaHints.classify(chain)` and `print(chain)` convert the safe zod subset
to `effect/Schema` text: `z.object`, `z.string`, `z.number`, `z.boolean`,
`z.array`, `z.enum`, `z.literal`, `z.union`, `z.record`, `z.int`, `.optional()`,
`.nullable()`, `.default()`, `.describe()`, and `.min()`/`.max()`/`.int()`/
`.nonnegative()` become `Schema.Struct`, `Schema.String`, `Schema.Number`,
`Schema.Boolean`, `Schema.Array`, `Schema.Literals`, `Schema.Literal`,
`Schema.Union`, `Schema.Record`, `Schema.Int`, `Schema.optional`,
`Schema.NullOr`, `Schema.withDecodingDefaultKey`, `.annotate({ description })`,
and `Schema.check(...)`.

Printed text needs `Schema` in scope, and `Effect` too when a field has a
default.

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.

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.                                                                                     |
| `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 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 `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, the report, and the
`proc:spawn` grant share, which for an argv is the kernel's own rendering of
the command it spawns.

## 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 `Flow.make({ description: "..." })` or `Flow.agent({ description: "..." })` with a string literal                                        |
| every flow module's descriptor describes the flow it declares | a default descriptor that admits an `input`/`output` no durable flow in the module declares, one whose `body` calls something else, or a lone descriptor with no behavior at all |
| 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` and `Flow.agent`,
the two constructors 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 is the other half of that. Discovery reads the default
export and never the named one, so a module whose descriptor declares a
different contract from its durable flow admits one thing and runs another. What
"the same" can mean is bounded by flows itself: `@smthrs/core`'s `body` returns a
`@smthrs/core/Node` while a durable flow's `.call` returns a `@smthrs/plan/Node`,
so a descriptor cannot delegate to a durable flow by body until the core-runtime
bridge lands, and the binding that can be written today is the contract: the
descriptor's `input` and `output` are the flow's `payload` and `success`, with
`{ ... }` fields and `Schema.Struct({ ... })` read as the same thing. A `body`
that does delegate is accepted, so the check does not have to change when the
bridge does. A module with no durable flow of its own has to carry its own
`body`, `model`, or `flows` instead, or calling it fails with `missing_body`.

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` exactly as the command printed it, and nothing redacts it. A
failing install or test suite in a 0.x project prints whatever it prints, a
registry token or a value read from `.env` included, and this tool cannot tell
a secret from a stack frame. 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 or a scanner-derived rewrite. The prompt never carries an invented identifier and never names a model.

## 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://migrate.smithers.sh/guides/clear-run-state/) and
[Accept constructs with no safe translation](https://migrate.smithers.sh/guides/allow-unsafe-constructs/).

## 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`        | `"flows"`                                                                     | The default flows directory.                                                                       |
| `defaultMaxRepairRounds` | `3`                                                                           | The default repair rounds.                                                                         |
| `reservedDirectories`    | `[".flows", ".git", ".jj", "node_modules"]`                                   | Directory names no layout path may be or live under.                                               |
| `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 not under `.flows`, `.git`, `.jj`, or `node_modules`); 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 layout path may be or live under.                                                                                                                                                                                                                                                                                                 |
| `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://migrate.smithers.sh/troubleshooting/#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 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 same rules grant `proc:spawn` one command line at a time, for this project's own install, format, typecheck, and test commands and nothing else. Those commands are what the manifests and the lockfile imply, as argv values the tool spawns with no shell, with the operator's `--verify-install`, `--verify-format`, `--verify-typecheck`, and `--verify-test` on top as the shell lines the operator typed; one derivation, `Layers.commandsFor`, serves the host and the units, because an agent shown one set of commands and granted another would be refused the very lines its brief lists, and the line a grant names is the kernel's own rendering of the argv it spawns. A spawned process writes at the OS level, where no `fs:write` rule can see it, so confining the spawn is the only place a shell can be stopped from reaching a database. What those commands then do is outside any rule, and the run-state digests are what catch it: the unit fails its checks and is restored.

### What a unit is allowed to write

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.

Everything after the tree is read runs inside one restoring scope. 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, the JSX compiler options and old path mappings removed, `.flows/` ignored. 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, declare no 0.x package in any of the six dependency fields, and pin `effect`; a project unit's tsconfigs still exist and configure no 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.                                                |
| `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 host that binds them.              |
| `mappingFlow`, `verifyFlow` | `CoreFlow.Flow`                                                                     | The two capabilities the rewrite reaches: mapping lookup and verification. |

### 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`                                                     | The JSX options and the old path mappings removed.                                                                    |
| `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.                                                                         |
| `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.0-rc.112"`                                                                                              | The `effect` version this release was built against.                                                                  |
| `smithersVersion`                                       | `"1.0.0-rc.0"`                                                                                                | The `@smthrs/*` version it adds.                                                                                      |
| `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 every run-state path, `proc:spawn` granted one command line at a time. |
| `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.                                                           |

### 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://model.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@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`.                                                                                                                                                                                                                           |

## `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`. `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.                                                                                             |
| `ReasoningEffort`      | literals | `"none" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh"`. 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`. 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 }`: a bounded model-boundary retry, recorded so run reports can count transport recovery. `delayMillis` defaults to `0`.                                                                                                                     |
| `Settle`                                          | struct     | `{ type: "settle", stopReason, responseId?, itemIds? }`. Ends the stream and states why; a stream without one was interrupted. `itemIds` carries stored provider reasoning items a continuation replays by reference.                                                                     |
| `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. |

## `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.                                                                                                                                            |
| `providerCode`       | `string?`        | The provider's own error code.                                                                                                                                                                    |
| `requestId`          | `string?`        | The provider's request id, from the usual `x-request-id` family of headers.                                                                                                                       |
| `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.

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        |
| `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 twelve-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. |

## `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 })`                                                   | constructor | `Result<Route, ModelError>`. Sends `x-api-key` and `anthropic-version: 2023-06-01`.                                                                                                                                                                                                            |
| `openai({ apiKey })`                                                      | constructor | `Result<Route, ModelError>`. Bearer auth against `api.openai.com`.                                                                                                                                                                                                                             |
| `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. When `Auth.refresh` is present, `Route.stream` runs it and retries
  the signed request exactly once, so an access token that expired mid-flight
  costs one recovery.
- **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 }`. `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` accepts pairs or a record.                                                                                              |
| `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. |
| `render(endpoint)` | formatting  | Renders the exact deterministic URL.                                                                                                                      |

`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 `refresh`: an effect `Route.stream` runs after an `authentication` failure before retrying 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? }`: the per-request policy the composed route supplies. Every request is wrapped in the kernel's `model:call` capability check for this `modelId`.                                                                          |
| `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. |
| `makeWith(transport)` | constructor               | `Effect<RequestExecutor>` over a transport it may replace.                                                                                                                                                                                             |
| `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 retry ladder: 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 bounded by the same
10 s per-wait cap; 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 and the retryable 503, 504,
and 529 to `provider_internal`; anything else to `unknown`. A protocol's
`classifyError` runs first and wins. 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,
so request traces redact these headers, including `chatgpt-account-id`.

`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.                                                   |

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"`. 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 deferred tools disabled.                                            |

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.

## `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.                                                   |
| `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` refuses a request that declares
tools, failing preparation with `invalid_request`, because providers reject
`tools` together with `response_format`. The one exception is
`toolChoice: "none"`: that lowering omits `tools`, so the two fields never
meet on the wire. Finish reasons map `stop`, `length`, `tool_calls`, and
`content_filter` onto the matching stop reasons. 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`.

## `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"`.                                                                                                                                                                                                                                                                           |
| `Resolution`                            | interface | `{ immediate, deferred, activatedNames }`: the tool partition derived from a sealed request.                                                                                                                                                                                                                            |
| `supportsDeferred(protocolId, modelId)` | predicate | Answers from explicit per-provider allowlists, matched case-insensitively. 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.

| 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.                                                                            |

## `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, or a cycle) 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.

---

# @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://notifications.smithers.sh)

> **Related APIs:** [`@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.

## 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
  ) => 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`.

### 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. |

### 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_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_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. `path` names the field that failed.                                  |

Storage failures surface as `Journal.JournalError` instead, so the two stay
distinguishable.

### 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. |

## 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 }`.                                                       |
| `Promoted`          | `{ boundary: string; targetLineageId: string; ids: ReadonlyArray<string> }`.                                         |
| `Event`             | `Admitted \| Promoted`.                                                                                              |

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. |

## 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)`,
which is what `NotificationQueue.layer` enforces. A deployment that raised the
bound with `NotificationQueue.layerWith` derives its own projection from
`NotificationState`, because this one would report a shorter queue than the run
actually holds.

## 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"`. Mirrors `ModelRequest.ReasoningEffort` in [`@smthrs/model`](/docs/reference/api/model/), which this package deliberately does not depend on. |
| `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` | `(payload: unknown) => SteerPayload \| undefined`           | The steering item a stored payload carries, or `undefined` when it is not one. A record with a `body` string and no `kind` reads as a message.    |
| `encode` | `(item: SteerPayload) => Readonly<Record<string, unknown>>` | The record the item is stored as. Every item is written with its `kind`, including a message. The result shares no mutable structure with `item`. |

## 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. |
| `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`.                                                                 |

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, never, HttpClient.HttpClient>` | POSTs each alert to one webhook. 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`. |

| Export        | Shape                                                                                                           |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `FailureCode` | `"sink_rejected" \| "sink_unreachable" \| "sink_timeout"`.                                                      |
| `AlertError`  | Tagged `/notifications/AlertError`: `{ code: FailureCode; message: string; status?: number; reason?: string }`. |

`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.

See [`@smthrs/control`](/docs/reference/api/control/) for the run conditions the entries come
from.

---

# @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://observability.smithers.sh)

> **Related APIs:** [`@smthrs/journal`](/docs/reference/api/journal/)

`@smthrs/observability` exports seven modules from its root entry point, and
each is also importable from `@smthrs/observability/<Module>`:

```ts
import { Endpoint, JournalLogger, Logger, Metric, Otel, Otlp, Resource } from "@smthrs/observability"
// or
import * as Otlp from "@smthrs/observability/Otlp"
```

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.0-rc.112`. 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`.

## Otlp

The default exporter. `Otlp` composes `effect/unstable/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
}
```

- `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.
- `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.

### 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.

### 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 22 and the only one that is
browser-safe by construction.

```ts
const Telemetry = Otlp.layerFetch({ baseUrl: "http://localhost:4318" })
```

### 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 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`.

## 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`, and no builder reads an
environment variable: every attribute is one the caller passed.

### 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. `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
```

### 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.

### 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 snapshots and redacts a bounded record synchronously, then performs
only queue admission; a forked worker drains the queue into the journal.

### 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 droppedLogRecords: Metric.Counter // "flows/observability/log/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.
- `droppedLogRecords` advances once per record lost by `JournalLogger`.

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 droppedLogRecords: typeof droppedLogRecords
}
```

All four handles as one object, for a host that enumerates them. The four
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 | undefined
  readonly loggerProvider?: LoggerProvider | undefined
  readonly metricReader?: MetricReader | ReadonlyArray<MetricReader> | undefined
  readonly loggerMergeWithExisting?: boolean | undefined
  readonly metricTemporality?: OtelMetrics.TemporalityPreference | undefined
}
```

`metricTemporality` is `"cumulative"` or `"delta"`.

### Otel.layerOtel

```ts
const layerOtel: (options: Options) => Layer.Layer<never, Resource.InvalidResourceConfiguration>
```

Bridges Effect's tracer, logger, and metrics onto the supplied providers and
readers, over the validated resource. Each bridge is installed only when its
provider is supplied, and an empty `metricReader` array is treated as no
metrics 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. 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, which keeps this module free
of Node imports and free of browser exporter policy.

### 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 processors 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://smithers-patterns.smithers.sh)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/)

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/core`](/docs/reference/api/core/) alone 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://smithers-patterns.smithers.sh/loops/), [Teams](https://smithers-patterns.smithers.sh/teams/), and [Delegation](https://smithers-patterns.smithers.sh/delegation/).
The [module index](https://smithers-patterns.smithers.sh/modules/) lists every module and where it is documented.

## The two halves of a pattern

Every container exports a pair.

| 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`. A handler runs `run`. The paired surfaces use the same
behavioral option names with call-shape differences: `Kanban` passes `items` as
the first argument to `run` and reserves `until` and `maxIterations` for
runtime branching; `MergeQueue` passes `members` as the first argument to
`make`. `Trellis` is the remaining exception: `run` additionally accepts
`continue` and `concurrency`.

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. The decorator
factories `WithRetry.make`, `WithCache.make`, and `WithApproval.make`
snapshot when `make` is called, not when the decorator is applied, and
`WithRetry.retryEffect` snapshots when it is called. `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:
`WithRetry` `backoff`, `Trellis` `envelope`, `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 values inside `Loop` `captures`, 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

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`, and none of them carries the
input that produced the failure.

### `PatternError`

`PatternError` carries a `code` from `PatternErrorCode`, a message, and an
optional `cause` with the reported error or errors.

| 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                                     |
| `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 |
| `finalizer_failed`    | A `TryCatchFinally` finalizer failed after the protected body settled                                               |
| `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, and
`Trellis.execute` and `Trellis.run` fail with one; a refusal `run` reports
carries `{ rounds, remaining }` as its cause, the rounds already executed and
the fuel left. `DelegationChain.run` fails with the same error when the
derisked plan does not fit its envelope.

| Code               | Path          | Raised when                                                                                                                                                                      |
| ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_envelope` | `root`        | A `fuel`, `depth`, or `fanout` bound, or a `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://smithers-patterns.smithers.sh/delegation/#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`, `maxDeriskRounds`, or `maxAttempts` 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 within `maxAttempts` attempts each                                                                                  |

## `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 its authority ceiling: 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.

The three `With*` modules are this combinator with a policy attached.

## `WithRetry`

`WithRetry.withRetry(flow, { attempts, backoff?, nonRetryable? })` wraps a flow
in a retry declaration, and `WithRetry.make(options)` is the same thing as a
decorator you can pass to `Pattern.decorateAll`. The wrapper preserves the
wrapped flow's graph and records the policy as declaration identity, so two
plans that differ only in an attempt budget are different declarations.
`WithRetry.retryEffect(effect, options)` performs the retry at the Effect
boundary, because a retry has no truthful form as a success-only `Node.andThen`
chain.

`attempts` is the total attempt count and must be a positive safe integer.
`backoff` is `{ initialMs, factor, maxMs }`, and the delay before attempt
`n + 1` is `min(initialMs * factor^(n - 1), maxMs)`. There is no jitter,
because a plan built twice has to describe the same waits. `nonRetryable` lists
error `_tag` values that end the sequence the first time one appears, whatever
the budget says. Fiber interruption is never retried, so a cancelled run stays
cancelled. The option names mirror `@smthrs/flow` `RetryPolicy`, so a pattern
policy and an engine policy translate one to one. See
[Retries](/docs/concepts/retries/).

## `WithApproval`

`WithApproval.withApproval(flow, { reason, approval })` runs an approval flow
ahead of the flow it gates, calling it with `{ input, reason, scope }`, where
`scope` is the string `"run"`. The approval flow must declare its output as
`WithApproval.Approved`, the literal `"approved"`. A denial therefore cannot
decode, and it fails on the typed schema-error channel before the gated flow
starts, rather than arriving as a boolean the gated flow is trusted to honor.
An empty `reason` is refused with `invalid_decorator`. `WithApproval.make`
is the decorator form.

## `WithCache`

`WithCache.withCache(flow, { ttlMs?, scope?, version? })` declares how long a
recorded result stays servable (`ttlMs`, counted from when it was recorded),
how far it may travel (`scope`, one of `"run"`, `"flow"`, and `"shared"`), and
which revision of the body produced it (`version`). Every field is optional and
every default is the behavior the composition already had. All three are
declaration identity, so a wrapper under one policy cannot serve a row recorded
under another, and `version` is what you change when the inputs stayed the same
but their meaning did not.

`WithCache.CachePolicyAnnotation` is the annotation the wrapper writes, under
the identifier `@smthrs/flow/Action/CachePolicy`, which is the key
`@smthrs/engine-store` reads at dispatch. `WithCache.policyOf(annotations)`
reads it back. The durable half is `ttlMs` and `scope`; `version` never reaches
dispatch, because it has already done its work in the key. A policy declared
here travels with the flow and enters its key material. To set the policy the
durable engine acts on when it dispatches a step, declare it on the action
itself with `CacheEnvironment.withCache` from [`@smthrs/flow`](/docs/reference/api/flow/), and
see [`@smthrs/step-cache`](/docs/reference/api/step-cache/) for what the engine does with it.

## `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. `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.

## `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
has to be a literal value 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.

## `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://smithers-patterns.smithers.sh/delegation/#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.

`Debate.run(input, options)` supplies the accumulated transcript instead:
`proponent` receives `{ input, transcript, round }`, `opponent` receives that
plus `proponent`, and `judge` receives `{ input, transcript }`. 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. `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 }`. A run that
spends every round returns `{ output, review, approved: false, exhausted: true }`
rather than failing, so the caller decides what an unapproved result is worth.
`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://smithers-patterns.smithers.sh/loops/#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 }` 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 }`.

`defaultEscalate` is the predicate used by `Escalation.run` when a rung has
neither an `escalateIf` nor a shared `accept`. `Escalation.make` instead
reserves every such rung because a declaration cannot branch on a value it
does not have. The runtime predicate 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.

## `TryCatchFinally`

`TryCatchFinally.make({ try, catch?, catchErrors?, finally? })` declares the
protected call, the recovery arm `catchErrors` selects, and a finalizer call on
the settled arm and on the arm no handler claimed. The unhandled arm ends in
`Node.fail`, so the plan states that the finalizer cleans up and hands the
failure back rather than absorbing it. Both arms are wrapped in `Node.capture`,
so the boundary keys the same way on every build.

`TryCatchFinally.run(input, options)` takes `catchErrors` as a predicate,
because the runtime form already holds the decoded typed error. The finalizer
runs after success, after recovery, after an unclaimed failure, and after
interruption. A finalizer that fails on its own becomes `finalizer_failed`; a
body failure outranks it, so cleanup trouble never hides the reason the body
failed.

## `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.

`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 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/).

## 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 `Loop.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
an unbounded loop, use the `run` half under an external scheduler instead of
unrolling it in `make`.

## 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://smithers-patterns.smithers.sh/modules/) lists all 28
modules with their specifiers.

`@smthrs/core` supplies `Flow`, `Node`, and `Graph`. 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://plan.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/crypto`](/docs/reference/api/crypto/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/keys`](/docs/reference/api/keys/)

`@smthrs/plan` exports ten modules from its root entry point, and each is also
importable from `@smthrs/plan/<Module>`:

```ts
import { FileSet, Node, Plan, PlanStore } 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.               |
| `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.                       |
| `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 reaches every layer:

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { Plan, PlanStore } 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" }
      }
    ]
  })

  const store = yield* PlanStore.PlanStore
  return yield* store.record(plan, Date.now())
}).pipe(Effect.provide(NodeCrypto.layer))
```

Compiling needs Effect's `Crypto` service and nothing else. Recording additionally needs `PlanStore.layer` over a `DurableWriter` and a `SqlClient`. The package depends on [`@smthrs/crypto`](/docs/reference/api/crypto/), [`@smthrs/database`](/docs/reference/api/database/), [`@smthrs/keys`](/docs/reference/api/keys/), and `effect`, and is browser-safe.

## 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, 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.

`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, 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`.

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.

### 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. 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. Existing key and digest formats remain unchanged; corrupted records are refused rather than silently rewritten.

### 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.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",
    "graph_too_large"
  ])
  message: Schema.String
})
```

A graph the compiler refuses. 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 |
| `graph_too_large`    | a plan contains more than `Plan.maximumPlanNodes` nodes                                                                                                                                                                                            |

Compilation walks with explicit stacks and never recurses per edge. The conflict and reader-after-writer passes compare node pairs, so pair comparison is quadratic in node count, and each pair whose write sets actually overlap adds one on-demand reachability walk over the edge set. A plan whose write sets barely overlap costs about `n²` comparisons; one whose writers overlap densely costs more than quadratic. `Plan.maximumPlanNodes` bounds that work, because a plan above it is refused with `graph_too_large` before any pair is compared.

### 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. 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 | 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.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/v4" | "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<A>
```

A node that succeeds with a constant.

### 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.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. 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, Exclude<E, Handled> | 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, Exclude<E, Handled> | 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 the values it accepts and preserves the remainder in the resulting error type.

### 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: <Args extends ReadonlyArray<unknown>, A>(
  captures: Readonly<Record<string, unknown>>,
  operation: (...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. The capture record is canonicalized into function identity and deeply frozen immediately. 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.

### 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`, and `functionIdentity` 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: "inline" | "boundary" | "handoff",
  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
```

`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.

## 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 letters, `..` 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.

### The declaration types

```ts
const Pattern: Schema.String // workspace-relative, with `*` and `**`
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.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), 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.

### FileSet.overlaps

```ts
const overlaps: (left: Entry, right: Entry) => boolean
```

Conservative static overlap. Exact paths compare in canonical separator and NFC form. 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.

## 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_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    |

`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_continuation`, `invalid_priority`, `invalid_payload`, and `cyclic_payload`; the rest come from [`@smthrs/flow`](/docs/reference/api/flow/)'s graph walk, which shares the vocabulary.

## 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 id and the same key are the same step. 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 fields the hashed material body folds: `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.

## PlanStore

[src/PlanStore.ts](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/plan/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.

`append` advances the plan row with a compare-and-swap on the previous generation, the flow, and the approved base digest, and refuses an append that adds no nodes. 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. Ordinals are derived from the rows already stored, not from the caller's array.

Every failure is a `PlanStoreError` whose `code` is one of `invalid_plan`, `constraint`, `decode_failed`, `persistence_failed`, or `unknown`.

### 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/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/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                      |
| `unknown`            | anything else, with `cause` carrying the original                                         |

`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/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://platform-browser.smithers.sh)

> **Related APIs:** [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/kernel`](/docs/reference/api/kernel/)

```ts
import { BrowserServices } from "@smthrs/platform-browser"
import * as Effect from "effect/Effect"
import { ChildProcess } from "effect/unstable/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, and `@smthrs/jj`,
whose wasm-backed `Jj` service the `BrowserHost` bundle composes.

:::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://platform-browser.smithers.sh/testing/).

`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. A host-backed `node:fs/promises` does not, 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`. 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.
[Read and write files on a mounted volume](https://platform-browser.smithers.sh/guides/work-with-files/#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`                                   | Uses backend `realpath`, preserving symlink resolution order. Fails with `PermissionDenied` when unavailable.                                                                                                                                                                                                                                                                                                                                       |
| `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.

Thrown backend errors are mapped onto `PlatformError` with the tag that carries
their meaning: `ENOENT` becomes `NotFound`, `EEXIST` becomes `AlreadyExists`,
`EACCES` and `EPERM` become `PermissionDenied`, `EISDIR`, `ENOTDIR`, and `ELOOP`
become `BadResource`, `EBUSY` becomes `Busy`, and anything else becomes
`Unknown`. The original error is kept as the `cause`.

## 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`                | `Sink.drain` and `Stream.empty`, the answer `NodeChildProcessSpawner` gives for an unconfigured descriptor                                                                                                            |
| `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.

## 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.

See [platform support](/docs/reference/api/#platform-support), 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/).

Publication delegates `rename` and `utimes` to the mounted backend. If either
method is absent, that operation fails with `PermissionDenied`. `realPath`
requires backend `realpath`; it never silently normalizes lexically.
The isolation layer requires workspace root `/` and fails typed otherwise.

The memory engine, adapters, and kernel can run in a tab. The durable engine
cannot: the shipped SqlClient uses `node:sqlite`, and NodeRuntime is Node-only.
BrowserHost exposes only `layer`; supply browser Crypto separately for hashing.
Opaque redirects are returned as successful HTTP responses with status 0.

---

# @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://platform-bun.smithers.sh)

> **Related APIs:** [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/platform-node`](/docs/reference/api/platform-node/)

```ts
import { BunHost } from "@smthrs/platform-bun"
import * as Effect from "effect/Effect"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make("printf", ["hello"]))
}).pipe(Effect.provide(BunHost.layer))
```

`@effect/platform-bun` is a required peer at exactly `4.0.0-rc.112` because
this barrel and `BunHost` import it at module load. It is installed alongside
the package by package managers that resolve required peers. The Effect Node
platform packages and `effect` are also exact peers at `4.0.0-rc.112`.

:::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.
:::

The complete host bundles require jj 0.39.0 or newer. Each bundle builds its jj
layer with one version probe; construction can fail with `JjError`, including
`not_installed` or `unsupported_version`. The contained bundles route that probe
through their process spawner and retire its ledger entry when it exits.

## 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 two names, `BunHost` and `BunFileSystem`,
each the namespace of the module above. `@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. Four layers, one error, three types, one
identity record, and four re-exports.

### Layers

| Export             | Signature                                                                                                     |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| `layer`            | `Layer.Layer<BunHost \| Crypto.Crypto, JjError>`                                                              |
| `layerAt`          | `(root: string) => Layer.Layer<BunHost \| Crypto.Crypto, JjError>`                                            |
| `layerContained`   | `(options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, JjError, ProcessLedger>`               |
| `layerContainedAt` | `(root: string, options?: ContainedOptions) => Layer.Layer<BunHost \| Crypto.Crypto, JjError, ProcessLedger>` |

`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; 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.

### BunHostError

```ts
class BunHostError extends Error {
  readonly name: "BunHostError"
  readonly code: BunHostErrorCode
  constructor(options: { readonly code: BunHostErrorCode; readonly message: string })
}

type BunHostErrorCode = "invalid_repository_root"
```

The refusal `layerAt` and `layerContainedAt` throw when `root` is not absolute,
the empty string included. 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` | The closed Host service union 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.        |

`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/Path",
  "effect/process/ChildProcessSpawner": "@effect/platform-bun/BunChildProcessSpawner",
  "@smthrs/jj/Jj": "@smthrs/jj/bun/BunJj",
  "effect/HttpClient": "@effect/platform-bun/BunHttpClient"
}
```

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/unstable/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 four 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`.                                  |
| `BunFileSystem`          | This package's `BunFileSystem` module.                                          |
| `BunHttpClient`          | `@effect/platform-bun/BunHttpClient`.                                           |

`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 python3 is not at `/usr/bin/python3` 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.

## 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 interpreter and byte limits.              |
| `Options`   | `AtomicFileSystem.Options`                | The interpreter, 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 a CPython 3 helper, which makes an
interpreter a host prerequisite: a `python3` supporting `O_NOFOLLOW`,
`O_DIRECTORY`, and `dir_fd` at `/usr/bin/python3`. Windows is unsupported.

`layerWith` is the escape hatch for a host whose python3 is not at
`/usr/bin/python3`, such as an alpine or nix image. `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.

## 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 `@effect/platform-bun`'s
fetch-backed `HttpClient` with `RequestInit { redirect: "manual" }`, 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://platform-bun.smithers.sh/concepts/host-surface/) explains what each slot
buys, [Runtime parity with Node](https://platform-bun.smithers.sh/concepts/runtime-parity/) explains how far
the two runtimes are interchangeable, and
[Troubleshooting](https://platform-bun.smithers.sh/troubleshooting/) lists the failures these exports
produce.

---

# @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://platform-node.smithers.sh)

> **Related APIs:** [`@smthrs/jj`](/docs/reference/api/jj/) · [`@smthrs/kernel`](/docs/reference/api/kernel/)

`@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/unstable/process/ChildProcess"
import { ChildProcessSpawner } from "effect/unstable/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 `NodeHttpClient.layerUndici`, which installs no redirect interceptor and
so leaves every hop visible to [@smthrs/kernel](/docs/reference/api/kernel/).

The complete host bundles require jj 0.39.0 or newer. Each bundle builds its jj
layer with one version probe; construction can fail with `JjError`, including
`not_installed` or `unsupported_version`. The version probe runs outside the host process ledger; repository commands
use the selected process runner.

## Requirements

| Requirement                                        | Why                                                                                                       |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Node.js >=22.19.0                                  | the minimum this package's `engines` field declares                                                       |
| CPython 3 at `/usr/bin/python3`                    | `AtomicFileSystem` runs every filesystem syscall through it                                               |
| that interpreter's `os` module supporting `dir_fd` | with `O_NOFOLLOW` and `O_DIRECTORY`, for `open`, `mkdir`, `readlink`, `rename`, `rmdir`, `stat`, `unlink` |
| a POSIX host                                       | Windows has none of those primitives and is unsupported                                                   |

The interpreter 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 on `node:22-slim`, `node:22-alpine`, or a distroless image
the layer builds, the run starts, and the first guarded filesystem call inside a
flow body fails `PermissionDenied`. Install `python3`, or point the adapter at
the interpreter you do have:

```ts
import * as AtomicFileSystem from "@smthrs/platform-node/AtomicFileSystem"

const filesystem = AtomicFileSystem.layerWith({ executable: "/usr/local/bin/python3" })
```

## 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 |

The barrel exports `NodeHost`, `HostLiveness`, `ProcessReaper`, and `ScopedProcess`.
`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 `@smthrs/flows`' `NodeRuntime` 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. 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.

| Constant or constructor               | What it is                                                            |
| ------------------------------------- | --------------------------------------------------------------------- |
| `AtomicFileSystem.layer`              | the adapter with every default                                        |
| `AtomicFileSystem.layerWith(options)` | the same, with the interpreter and the ceilings configured            |
| `AtomicFileSystem.defaultExecutable`  | `/usr/bin/python3`                                                    |
| `AtomicFileSystem.defaultLimits`      | 16 MiB content, 24 MiB request, 24 MiB response, 64 KiB helper stderr |
| `AtomicFileSystem.defaultConcurrency` | `os.availableParallelism()`                                           |
| `AtomicFileSystem.defaultTimeoutMs`   | 300000                                                                |
| `AtomicFileSystem.program`            | the source text of the POSIX helper the adapter runs                  |

**Cost.** Every operation is one CPython fork, roughly 130 ms on a current host.
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 interpreters at once. Batch a wide fan-out, or raise
`concurrency` deliberately. A directory listing is one fork 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. 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 is segment-wise and linear in the candidate's
length, never a compiled regular expression, because a pattern of repeated `*x`
fragments costs a regex engine exponential backtracking.

Two grammar bounds are refusals rather than silent truncation: a pattern longer
than 4096 characters, and one whose braces expand past 64 alternatives, both
fail as `BadArgument`. Both are enforced before any expansion or any 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)`), POSIX classes (`[[:digit:]]`), brace
ranges (`{1..3}`). Each of them means something to the
native globber, so reading them as ordinary characters would not fail; it would
answer a different question, and in an exclusion that means handing the caller
the very paths it forbade. 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 `**`.** On 22.19.0 `**/.hidden` matches nothing and
   on 24 it matches the dotfiles. The adapter follows the newer reading.

**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

`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 questions put to `ps`: this process's own group, and when the
recorded pid started. Either can go unanswered on a host with no usable one, and
an unanswered guard refuses, because a guard that did not run is not a guard
that passed. No evidence never authorizes a `SIGKILL`.

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.

## 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.

---

# @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://plugin.smithers.sh)

> **Related APIs:** [`@smthrs/canonical`](/docs/reference/api/canonical/) · [`@smthrs/flow`](/docs/reference/api/flow/)

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`.

## 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.                                                     |

### FlowsHooks

```ts
interface FlowsHooks {
  readonly config: WaterfallHook<(config: FlowsConfig) => Effect.Effect<Partial<FlowsConfig> | void, any, any>>
  readonly configResolved: ParallelHook<(config: ResolvedConfig) => Effect.Effect<void, any, any>>
}
```

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.

## 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 nested arrays and falsy entries, 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.                    |

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>
}

const make: <H = FlowsHooks>(
  input: PluginInput<NoInfer<H>>,
  config?: FlowsConfig,
  options?: Omit<Resolve.Options, "config">
) => Effect.Effect<Kernel<H>, PluginError>

const runConfig: <H = FlowsHooks>(
  plugins: Plugins.Service<H>,
  config: FlowsConfig
) => Effect.Effect<ResolvedConfig, PluginError>
```

`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. `runConfig` exposes step 3 and step 4 on their own, for a
host that resolved its plugin list separately.

`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.                                              |

A waterfall handler that returns `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 parallelConcurrency: number
  readonly cacheEnvironment?: Action.CacheEnvironment | undefined
}

interface Options {
  readonly config?: FlowsConfig | undefined
  readonly target?: "engine" | "harness" | undefined
  readonly hooks?: Readonly<Record<string, HookKind>> | undefined
  readonly cacheEnvironment?: Action.CacheEnvironment | undefined
  readonly parallelConcurrency?: number | undefined
}

const resolve: <H = FlowsHooks>(
  input: PluginInput<NoInfer<H>>,
  options?: Options,
  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. |
| `target`              | `"engine"`    | The host whose literal `apply` selectors are active.                                                                    |
| `hooks`               | `engineHooks` | The hook names and kinds this host recognizes. The `unknown_hook` guard checks against it.                              |
| `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. When the resolution declared a
cache environment, `layer` also provides
`Action.CurrentCacheEnvironment`.

### Limits

| Constant                     | Value |
| ---------------------------- | ----- |
| `maximumPlugins`             | 256   |
| `maximumHandlers`            | 1,024 |
| `maximumPluginInputNodes`    | 4,096 |
| `maximumPluginDepth`         | 64    |
| `maximumPluginNameLength`    | 256   |
| `defaultParallelConcurrency` | 16    |
| `maximumParallelConcurrency` | 256   |

Exceeding one fails with `resource_limit` and the path of the offending entry.
The handler bound counts the handlers the kernel dispatches. Plugin, version,
and hook names are control-free, well-formed strings within
`maximumPluginNameLength` that are neither empty nor entirely whitespace.

## Config

```ts
const ConfigValue: Schema.Json
type ConfigValue = typeof ConfigValue.Type

const FlowsConfig: Schema.Record$<Schema.String, typeof ConfigValue>
type FlowsConfig = typeof FlowsConfig.Type

const ResolvedConfig: Schema.Record$<Schema.String, typeof ConfigValue>
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. Both operands and the result are admitted, so a chain of small patches cannot exceed the 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.                                                                                                                                                                                            |

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`.

## PluginError

```ts
const PluginErrorCode: Schema.Literals<[
  "duplicate_name",
  "unknown_hook",
  "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 JSON pointer into the refused value (`$[2].hooks.config`
for the third plugin's `config` entry, `$options.parallelConcurrency` for an
option), and `cause` carries a failing handler's or layer's original failure.
Match on `code`; the messages are prose.

| Code                        | Meaning                                                                        |
| --------------------------- | ------------------------------------------------------------------------------ |
| `duplicate_name`            | Two selected plugins have the same exact name.                                 |
| `unknown_hook`              | A plugin declares a hook absent from the host catalog.                         |
| `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://plugin.smithers.sh/troubleshooting/).

## 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 one other runtime dependency this package
has.

---

# @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://registry.smithers.sh)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/plan`](/docs/reference/api/plan/)

`@smthrs/registry` exports eight 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.                                  |
| [`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.                |
| [`Pack`](#pack)                   | Manifests, content addresses, compatibility ranges, and merge precedence. |
| [`RegistryError`](#registryerror) | The typed failures and their constructors.                                |

## 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://registry.smithers.sh/concepts/descriptors/) 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<string>
  readonly flows: ReadonlyArray<string>
  readonly capabilities: ReadonlyArray<string>
  readonly effects: EffectDeclaration
  readonly placement: Option.Option<Placement>
  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.

### 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.

### 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
}
```

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.

### 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 BodyRefModule {
  readonly _tag: "Module"
  readonly path: string
  readonly contentDigest?: 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`
rehashes markdown source, and `Executable.fromDescriptor` verifies module source
before importing it; a mismatch is `body_unavailable`.

### 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"
}
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.
`None` is a flow that declared neither.

`Inline` is the one variant that carries a schema by value, as a
`Schema.toJsonSchemaDocument` output kept as plain JSON. The other four are
locators; 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.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://registry.smithers.sh/concepts/authority/).

`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 FlowBudget: Schema.Struct<{
  tokens: Schema.optional<typeof BudgetCeiling>
  milliseconds: Schema.optional<typeof BudgetCeiling>
}>

const budgetUnbounded: FlowBudget
const budgetOf: (descriptor: FlowDescriptor) => FlowBudget
```

The tokens and milliseconds a flow declares that a control plane should approve
for one of its runs. Both are positive safe integers, so the schema refuses
zero, a negative, a fraction, `NaN`, and anything past
`Number.MAX_SAFE_INTEGER`, and both survive durable JSON unchanged.

`budgetUnbounded` is the budget of a flow that declares neither 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.

### 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 30 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_model_invocation`, `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`, `max_depth_exceeded`, `entry_too_large`, `unreadable`                                                                                           |
| Packs                  | `unknown_pack_key`, `shadowed`                                                                                                                                                                                                                      |

Each code, with its cause and its fix, is in
[Diagnose a flow that did not appear](https://registry.smithers.sh/guides/diagnose-a-missing-flow/).

## 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.

### 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.

The size ceiling is checked twice, because the first check trusts the host. A
`stat` size past the limit skips the file unread, which is the fast path an
ordinary file system takes; the bytes actually read are then measured against
the same limit, so a host whose `stat` under-reports or omits a size still gets
`entry_too_large` with the true byte count, and the entry is refused before it
is hashed, decoded, or parsed.

`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 `None` when the flow has no non-empty `description`, which is
the one field discovery requires. Everything else that is missing or malformed
produces a warning and a conservative value. `warnings` is non-empty in far
more cases than that, so a caller reports it either way.

### MarkdownFlow.loadBody

```ts
const loadBody: (text: string, baseDirectory: string) => FlowBody
```

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: FlowBody & 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.

## Registry

The refreshable, first-found-wins catalog a host consumes.

### Registry.Registry

```ts
interface Registry {
  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. Markdown bytes are checked against the discovery digest. |
| `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 observe one complete snapshot, so a `list` and the `get` after it never
disagree. `refresh` replaces the snapshot only after every source succeeds, so
a failed rescan leaves the previous complete snapshot serving reads rather than
emptying the catalog.

`loadBody` and `runPrompt` are the only two members that touch the filesystem.

Pass the plan's `executionDigest` to `loadBody` when loading an approved flow.
A descriptor that no longer matches it fails with `execution_changed`, before
body loading. A markdown file changed after discovery fails with
`body_unavailable`. Refreshing the registry does not authorize the new work;
create and approve a new plan.

### Registry.Config and Registry.PackConfig

```ts
interface Config {
  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.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://registry.smithers.sh/guides/load-packs/).

The host that calls `Pack.read` must surface its manifest warnings before
projecting that result to `Installed`, whose public shape retains none.

### 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://registry.smithers.sh/guides/show-flows-to-a-model/).

### 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://registry.smithers.sh/concepts/delegation/) explains the model.

### Executable.Executable

```ts
interface Executable {
  readonly descriptor: Descriptor.FlowDescriptor
  readonly delegate: string
  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 its body is one delegating node. `layer` registers it with
the runtime.

### Executable.Options

```ts
interface Options {
  readonly delegates: ReadonlyArray<Delegate>
  readonly agent?: string | undefined
  readonly load?: ((path: 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` replaces the default dynamic `import` of
the module a descriptor points at; it receives a filesystem path, not a
specifier.

### 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.

`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://registry.smithers.sh/guides/reuse-a-flow-result/).
`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 `Flow.make({ 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.

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>
}

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.

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: Options) => Layer.Layer<
  Catalog,
  RegistryError | DiscoveryError,
  Registry.Registry | FileSystem.FileSystem | Path.Path | Registration
>
```

Registers every runnable discovered flow with the runtime. 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.

### 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.ProjectOptions and Executable.layerProject

```ts
interface ProjectOptions {
  readonly root: string
  readonly packs?: Registry.PackConfig | undefined
}

const layerProject: (options: ProjectOptions) => Layer.Layer<
  Registry.Registry,
  RegistryError | DiscoveryError,
  FileSystem.FileSystem | Path.Path
>
```

The registry a Node host discovers a project in: `<root>/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 `flows/` directory is not a failure: it has no flows yet,
which is the state [`smthrs init`](/docs/reference/cli/init/) leaves behind. That is decided
by looking for the directory up front, so the answer stays a statement about
the project. Catching the scan's `root_missing` instead would make a pack that
declares a directory it does not ship read as "this project has no flows" and
empty the registry the project's own flows were in.

### 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: "missing_delegate" | "ambiguous_delegate" | "body_unavailable" | "invalid_module"
  readonly flow: string
  readonly path?: string
  readonly delegate?: string
  readonly available: ReadonlyArray<string>
  readonly message: string
  readonly cause?: unknown
}
```

A descriptor the bridge will not turn into a runnable flow. `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.

Each code, with its cause and its fix, is in
[Troubleshooting](https://registry.smithers.sh/troubleshooting/).

## Pack

Workflow packs: a directory of flows with a manifest, a content address, and a
merge order. See [Load workflow packs](https://registry.smithers.sh/guides/load-packs/).

`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
}

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`. Surface 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 an ordinary path-named source rooted inside the pack, so
a pack is discovered by exactly the pipeline a project directory is. `source`
carries `pack:<name>`, which is what a warning about a pack file reads back.

Lexical containment is always enforced. When both real paths are available,
real-path containment also refuses symlink escapes; hosts that cannot answer
`realPath`, and sources not created yet, use the lexical verdict. The defense
is repeated here because callers may construct an `Installed` value without
decoding a manifest first.

### Pack.checkCompatible, Pack.compatible

```ts
const compatible: (range: string, runtimeVersion: string) => boolean

const checkCompatible: (
  pack: Installed,
  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 and omitted components are zero-filled. `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`. `~` pins the minor, so `~1.2.0` accepts `1.2.9`
and refuses `1.3.0`.

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.

### 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.

## 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 the offending `path` as a field rather than only inside the
prose message, so a caller can act on it without parsing text. Each code, with
its cause and its fix, is in [Troubleshooting](https://registry.smithers.sh/troubleshooting/).

### 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.

---

# @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://run-store.smithers.sh)

> **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/).

## 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`.

#### 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.
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. A completed predecessor does not hide its live handoff
successor. Existing request times and completed-round history are unchanged.
`CancelRequested` takes precedence if any round gets a new request;
otherwise a previous request yields `AlreadyRequested`. With every round
settled, `Terminal` describes the latest round. An unknown ID yields `NotFound`.
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. `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.

#### 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>
```

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.

### 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 exact triple every claim guards. `running` requires both an owner and a
heartbeat; every other status requires neither. Extra properties are 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://run-store.smithers.sh/troubleshooting/).

### 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.

### 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.

#### 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`.                           |
| `cross-host-unreachable-stale` | The hosts differ.                                                               |
| `lease-expired`                | Any host. The store verifies this claim itself against the persisted heartbeat. |

`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. Browser compositions keep
it, because a tab has no process table to ask.

### 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.

### heartbeatLoop

```ts
const heartbeatLoop: (runId: string, owner: OwnerId) => Effect<never, never, RunStore>
```

Pulses every `heartbeatInterval` on the injected `Clock` 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; a failed heartbeat write is tolerated for
`heartbeatWriteTolerance`, and every successful pulse re-arms that window.

### 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://run-store.smithers.sh/guides/observe-outcomes/).

## 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. 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://run-store.smithers.sh/concepts/execution-revisions/).

## Heartbeat

`@smthrs/run-store/Heartbeat` is a leaf module holding the four lease durations
and the one place they are related. `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://run-store.smithers.sh/concepts/leases/).

## 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://run-store.smithers.sh/guides/testing/).

## Related reading

- [Fencing and ownership](https://run-store.smithers.sh/concepts/fencing/) for why competition is a value.
- [Durable values](https://run-store.smithers.sh/concepts/durable-values/) 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.

---

# @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://sandbox.smithers.sh)

> **Related APIs:** [`@smthrs/kernel`](/docs/reference/api/kernel/)

`@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/unstable/process/ChildProcess"
import { ChildProcessSpawner } from "effect/unstable/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/DirectorySandbox`          | [src/DirectorySandbox/](https://github.com/smithersai/smithers/tree/main/packages/smithers/flows/sandbox/src/DirectorySandbox)                   |
| `@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)               |

## 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.

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: the same string `@smthrs/kernel`'s `proc:spawn` check is written against, so a grant and the thing it authorizes cannot drift apart.

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, so it turns `false` when a caller observes `exitCode` rather than when the remote process actually ends.
:::

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 `ProviderError` and its `cause` never reach a logger from the probe: adapters attach raw vendor errors to `cause`, which can quote credentials, request headers, proxies, or response bodies, and rendering an arbitrary object can throw or run without bound, so raising the minimum log level to `Debug` discloses nothing. 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 everything running 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. It 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 is ordered around what is mandatory. Failing the in-flight commands and closing the provider scope run first and uninterruptibly; only then is the verdict reported. A `Reporter` is caller supplied and observational, so it may defect, be interrupted, or never return, and none of that may strand a waiter, leak the machine, or hold the one permit every later command needs. A reporter that outlives 30 seconds is abandoned and its failure is logged.

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` (240 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. Acquisition registers teardown as a finalizer of that scope; closing the scope is the only lifecycle end exposed to the caller. The stable key lets an implementation deterministically name and reattach a crash-left machine when it can. Image, memory, network policy, and other machine shape belong to provider construction, not `acquire`.

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.fromProvider`, `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. Directory output is line framed, so a newline in a filename is misread; the listing probe is `ls -1A` and never a bare `ls -A`, because POSIX `ls` columnizes when its output is a terminal and one provider's transport is a pseudo-terminal. 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.fromProvider` 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.

### Default provider

`Sandbox.selectProvider(registry, name?)` picks the provider a host composes when a flow or agent names none. `Sandbox.defaultProviderName` is `"microsandbox"`: a microVM, which is the strongest boundary a bundled provider offers and the one that can hold a Nix environment its composer supplies. Every other provider stays selectable by name (`"directory"`, `"container"`, `"kubernetes"`, `"just-bash"`, `"vercel"`, `"daytona"`, `"aws"`, `"cloudflare"`). A name the registry does not hold, the default included, fails with `ProviderError.code === "unavailable"` listing what is registered; nothing falls back to a weaker sandbox silently.

## SandboxConformance

```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.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 nine.

| 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                                                     |
| `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`                                                      | yes, `AlreadyExists` 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                                                               | yes                                                  | 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             | no                                                               | yes, the SDK's byte-typed output                     | 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"
})
```

`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.

### 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`; setting `network` to another engine mode is an explicit egress opt-in. `acquire` deterministically names the container, runs `create` then `start`, and reattaches when that name already exists: a refused create is followed by `container inspect`, so the reattach turns on whether the engine has the container rather than on the English wording of its conflict message. 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.

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.

### 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`, treats an `AlreadyExists` answer as a reattach, 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.

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 spawn completes before it returns, stdout and stderr each replay at most one 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.

### MicrosandboxSandbox

```ts
import { MicrosandboxSandbox } from "@smthrs/sandbox"
import * as Microsandbox from "microsandbox"

const provider = MicrosandboxSandbox.make({
  sdk: Microsandbox,
  image: "oven/bun:1",
  persistence: "sticky",
  cpus: 2,
  memoryMib: 2048,
  idleTimeoutSecs: 600,
  disableNetwork: true
})
```

The machine name is `smthrs-msb-<slug>`. `acquire` configures the builder and calls `create`; when the SDK answers `sandboxAlreadyExists` it fetches the handle 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 stopped. `ephemeral` persistence, the default, stops the machine when the scope closes; `sticky` deliberately leaves it running so the next acquire of the same session key can reconnect, and only stops a sticky machine it created but could not prepare. 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, and the single-drain handle is collected exactly once; output comes back through the byte-typed `stdoutBytes()` and `stderrBytes()`. 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`.

`image` and `snapshot` are exclusive; naming both fails with `unavailable` before any vendor call. Output is collected after the command finishes: one stdout chunk, one stderr chunk, no streaming, and no `kill`. A stopped ephemeral machine is gone with its files. The real-backend suite runs the conformance check against a real microVM only where the `microsandbox` binary answers `--version` and the host can actually boot one.

`environment` plants the workspace's Nix environment in the microVM and runs every command under it:

```ts
const provider = MicrosandboxSandbox.make({
  sdk: Microsandbox,
  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`.

`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.

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. The lowest ARN is adopted and waited for if it is not ready; anything else under that key is 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 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, and 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 per-command nonce, 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 is base64 inside one `--command` argv entry. Base64 expands by four thirds, and Linux caps one argv entry at 128 KiB (`MAX_ARG_STRLEN`, 32 pages), so 64 KiB of payload plus framing stays below the smallest known limit on the path. AWS publishes no separate SSM document limit. 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.

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 the transport 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. AWS does not publish an exact SSM command limit, so `chunkBytes` enforces the portable 65536-byte payload bound derived from Linux's per-argument cap instead of claiming a service-specific limit.

### 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://sandbox.smithers.sh/limits/) 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://sandbox.smithers.sh/guides/place-a-flow-body-on-a-machine/),
[run commands through a transport](https://sandbox.smithers.sh/guides/run-commands-through-a-transport/),
[choose a provider](https://sandbox.smithers.sh/guides/choose-a-provider/),
[supervise a session](https://sandbox.smithers.sh/guides/supervise-a-session/),
[write a provider](https://sandbox.smithers.sh/guides/write-a-provider/),
[prove a provider](https://sandbox.smithers.sh/guides/prove-a-provider/), and
[test against a scripted machine](https://sandbox.smithers.sh/guides/testing/). The model behind them
is in [the two provider seams](https://sandbox.smithers.sh/concepts/seams/),
[sessions and their keys](https://sandbox.smithers.sh/concepts/sessions/),
[what a sandbox does and does not prevent](https://sandbox.smithers.sh/concepts/isolation/), and
[how a remote command differs from a local one](https://sandbox.smithers.sh/concepts/remote-commands/).

[`@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://scorers.smithers.sh)

> **Related APIs:** [`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/journal`](/docs/reference/api/journal/)

`@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. Top-level modules
are also importable directly as `@smthrs/scorers/<Module>`. The `internal/*`,
`migrations/*`, and nested `*/index` subpaths are blocked, so the four
migrations are reachable only through the root `Migrations` namespace. For the
import forms, see [Installation](https://scorers.smithers.sh/installation/#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://scorers.smithers.sh/exports/).

```typescript
import { Runner, RunnerLive, Scorer, ScoreStore, SqlScoreStore } from "@smthrs/scorers"
import { Effect, Layer } 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*() {
  const runner = yield* Runner.Runner
  return yield* runner.runBatch([{
    identity: Runner.jobIdentity(["run-1", "step-7", quality.scorerKey]),
    observation: { targetStepKey: "step-7", scorerKey: quality.scorerKey },
    score: quality.score({ input: "ask", output: "expected" }),
    at: Date.now()
  }])
})

const live = RunnerLive.layer({ concurrency: 4 }).pipe(Layer.provide(SqlScoreStore.layer))
const inert = RunnerLive.layer().pipe(Layer.provide(ScoreStore.layerNoop))
```

For a runnable composition including the database layers, see the
[Quickstart](https://scorers.smithers.sh/quickstart/).

## 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://scorers.smithers.sh/guides/grade-fixed-samples/)
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`, and `body`, 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.

`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://scorers.smithers.sh/concepts/scorer-identity/).

`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 `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.

## 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://scorers.smithers.sh/troubleshooting/).

## Runners

`Runner.Service` has three entry points and one execution path. `submit` queues
a job and returns: it does not wait for the scorer to run, but it backpressures
once `capacity` queued jobs are outstanding, so it is not safe on a
latency-critical path. `runBatchCorrelated` runs jobs at the configured
concurrency and returns an `Outcome` for each one. Each outcome carries the job
identity and reports `persisted`, `duplicate`, or `failed` for the durable
write. `runBatch` is derived from it and returns only the observations, in job
order.

Both are governed by one rule: a scorer failure becomes an inconclusive
observation and never fails the target or the batch. Fiber interruption still
propagates. A score-store failure is logged as a warning and does not fail the
batch either. `runBatch` records what each scorer _answered_, while
`runBatchCorrelated` also records what the store did. A `duplicate` outcome
means the identity was already claimed; it does not claim that the returned
observation is the one already in the store.

`RunnerLive.layer` coerces a `concurrency` or `capacity` that is not a positive
safe integer to its default (1 and 1024) rather than failing, because the
layer's error channel is `never`. The layer is scoped: it forks its workers
into the scope that builds it, so a job still queued when that scope closes is
never scored.

`Runner.layerNoop` and `ScoreStore.layerNoop` are the explicit absences.
Neither fails; the runner accepts every job and runs nothing, and the store
accepts every write and reads back nothing.

## Stores

`ScoreStore.Service` has four 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.

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://scorers.smithers.sh/durability/).

## Snapshotting

Three handoffs decide what a later mutation can still change, and each one has
one answer.

`submit` copies a job's scalar fields as it queues it, and `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
`Runner.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`, and `body`.                 |
| `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, ground truth, context, and sampling policy attached to a target 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.ScoreStore`              | services     | Context service for durable scorer observations.                                |
| `ScoreStore.make`                    | constructors | Constructs a score store.                                                       |
| `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.                                            |
| `Runner.Job`                         | models       | One scorer execution request and its durable idempotency key.                   |
| `Runner.BatchOptions`                | models       | Batch execution options.                                                        |
| `Runner.Recorded`                    | models       | Whether a batch observation was persisted, duplicated, or failed.               |
| `Runner.Outcome`                     | models       | A batch observation tagged with its job identity and durable write result.      |
| `Runner.Service`                     | services     | The runtime scorer runner implementation.                                       |
| `Runner.Runner`                      | services     | Context service for live and batch scorer execution.                            |
| `Runner.make`                        | constructors | Constructs a scorer runner.                                                     |
| `Runner.makeNoop`                    | constructors | Constructs an inoperative scorer runner.                                        |
| `Runner.layerNoop`                   | layers       | Provides the inoperative scorer runner.                                         |
| `Runner.jobIdentity`                 | constructors | Builds a length-prefixed `Job.identity` from its components.                    |
| `Runner.inconclusive`                | converting   | Converts a scorer failure into a typed inconclusive observation.                |
| `RunnerLive.Options`                 | models       | Live runner worker configuration.                                               |
| `RunnerLive.layer`                   | layers       | Provides the scoped queue and the blocking batch runner.                        |
| `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.                            |

---

# @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://std.smithers.sh)

> **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/)

The package exports 31 modules. Each is reachable from the root entry point as a
namespace and from its own subpath:

```ts
import { Read } from "@smthrs/std"
import * as Read from "@smthrs/std/Read"
```

`@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://std.smithers.sh/reference/flows/). This page is the module and export
surface.

## 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`                                 | The input schema.                        |
| `Output`       | `Schema`                                 | 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`, and `Input` and `Output` as TypeScript types beside the schemas       |
| `Explore`      | `make(options: { model?: string })`, and no `run`                                           |
| `Grep`         | `ContextLine`, `Symbol`, `Match` schemas                                                    |
| `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`                                    |

## 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 * as Manifest from "@smthrs/std/Manifest"

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? }`, tagged `flows/std/StdError`. |

```ts
import * as StdError from "@smthrs/std/StdError"

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://std.smithers.sh/reference/flows/#failures).

## 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 }`.                                                            |
| `classify`     | `(result: { exitCode, stdout, stderr }) => InvalidProbe \| undefined` | Classifies one command result.                                                              |

```ts
import * as Probe from "@smthrs/std/Probe"

const probe = Probe.classify({ exitCode: 127, stdout: "", stderr: "pytest: command not found" })
// { reason: "unknown-command", evidence: "pytest: command not found", message: "..." }
```

A zero exit is never classified. A runner's own report that it executed tests
vetoes every wording recogniser, so a genuine reproduction that prints
`No module named` from inside a test is not suppressed. Exit codes 126 and 127
are the shell's verdict and are not vetoed.

## 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>`          |

## 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>) => 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`.

## 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. |

## 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`: the argv the host spawns.                 |
| `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`.

## 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?`, `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 `flows/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`      | `StdError`                                                                 | The refusal a host that pins nothing answers with.             |
| `makeNoop`         | `() => Checkpoints`                                                        |                                                                |
| `layerNoop`        | `Layer<Checkpoints>`                                                       |                                                                |
| `GitOptions`       | interface                                                                  | `root`, `cwd?`, `baseRef?`.                                    |
| `makeGit`          | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>` |                                                                |
| `layerGit`         | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>`  |                                                                |
| `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.

## 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 methods, each returning `Effect<unknown, StdError>`.            |
| `make`           | `(service: LanguageServer) => LanguageServer` |                                                                     |
| `makeNoop`       | `() => LanguageServer`                        | Answers `unsupported` for every request.                            |
| `layerNoop`      | `Layer<LanguageServer>`                       |                                                                     |

The methods are `hover`, `definition`, `references`, `implementation`,
`documentSymbols`, `workspaceSymbols`, `prepareCallHierarchy`,
`callHierarchyIncoming`, `callHierarchyOutgoing`, and `diagnostics`.

## NodeLanguageServer

A host LSP client over the permission-checked process spawner, speaking framed
JSON-RPC on ordinary stdio pipes.

| Export                 | Type                                                                                 | Meaning                                                  |
| ---------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| `Config`               | interface                                                                            | `command`, `args?`, `cwd`, `environment?`, `timeoutMs?`. |
| `MAX_QUEUED_FRAMES`    | `256`                                                                                | Frames buffered for the server's standard input.         |
| `MAX_PENDING_REQUESTS` | `512`                                                                                | Concurrent in-flight requests.                           |
| `make`                 | `(config: Config) => Effect<LanguageServer, StdError, ChildProcessSpawner \| Scope>` |                                                          |
| `layer`                | `(config: Config) => Layer<LanguageServer, StdError, ChildProcessSpawner>`           |                                                          |

`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.

## 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://std.smithers.sh/guides/reach-the-network/#search-the-web) for the status
mapping.

## Runtime notes

The root entry point is Node-only: it re-exports `NodeLanguageServer`, which
imports `node:url`. The four browser-safe subpaths are `@smthrs/std/Grep`,
`@smthrs/std/Glob`, `@smthrs/std/Search`, and `@smthrs/std/PortableSearch`.
Nothing any of the four imports reaches a Node built-in.

The model behind this surface has its own pages:

- [Limits are disclosed, never silent](https://std.smithers.sh/concepts/limits-and-disclosure/) for
  every cap and how it is reported.
- [Effects and capabilities](https://std.smithers.sh/concepts/effects-and-capabilities/) 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://step-cache.smithers.sh)

> **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://step-cache.smithers.sh/concepts/head-and-ledger/).

## 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) => 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. The ledger is never swept.                     |

`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://step-cache.smithers.sh/guides/read-a-recorded-result/) and
[evict a poisoned entry](https://step-cache.smithers.sh/guides/evict-a-poisoned-entry/).

### 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://step-cache.smithers.sh/concepts/admission/).

### 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 one that does: two runs recorded different results
under one digest. How the two stages arbitrate is in
[the head and the ledger](https://step-cache.smithers.sh/concepts/head-and-ledger/).

### 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://step-cache.smithers.sh/troubleshooting/) 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 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://step-cache.smithers.sh/guides/compose-a-store/).

### 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://step-cache.smithers.sh/guides/test-with-the-cache/).

### 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.

### 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`. Accessors, symbol keys, extra enumerable
members, and non-plain shells are refused with `invalid_cache` without running
caller code.

## 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://step-cache.smithers.sh/guides/observe-cache-outcomes/).

### 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://step-cache.smithers.sh/concepts/tiers/).

### 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://step-cache.smithers.sh/guides/implement-a-shared-tier/).

### 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.

### 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.

### 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://step-cache.smithers.sh/quickstart/) 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://smithers-sync.smithers.sh)

> **Related APIs:** [`@smthrs/journal`](/docs/reference/api/journal/)

```ts
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.Sync
  return sync.subscribe({ scope: { _tag: "Run", runId: "build-42" }, 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;
`WorkspaceShare` additionally carries a `kid` so keys rotate without
invalidating capabilities minted under a retired one.

## Bounds

Every fan-out surface is bounded, so one follower's cost is a function of the
configured bound rather than of the workspace's size or of how far behind that
follower has fallen.

| 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. Each round reads one bounded page per covered run, 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.                                                                          |
| `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.make` `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.     |
| `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 value that is not a
positive safe integer fails the constructor with `invalid_request` instead of
quietly disabling the comparison it configures.

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. 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`: separate
`{ _tag: "Delivered", cursors }` and `{ _tag: "Applied", cursors }` fields.
`cursors` on the service remains a delivery bookmark. Applied progress 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.Sync` is the browser-safe service tag. `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.

Server responses are admitted, never trusted, on both paths. A frame or page
whose encoded entries exceed `maxFrameBytes` is refused with
`frame_too_large`; one that carries another run's entry, repeats or reorders a
sequence, or serves an entry at or below the cursor the request carried is
refused as a `protocol_violation` before any cursor moves; and an incomplete
page that makes no progress fails typed instead of re-reading forever.

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 `AppliedProgress`: 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.

## 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 }`. 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. 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

`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

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.
:::

## Test helpers

| Export                                                             | Source                                                                                                                         | Notes                                                  |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `TestSync.layerTest`, `layerWorkspaceAuth`, `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://smithers-sync.smithers.sh/guides/run-a-long-soak/) 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.

---

# @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://testing.smithers.sh)

> **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 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"
```

Three 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.

`@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.

## 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://testing.smithers.sh/concepts/test-tiers/).

### 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 onConflict?: string
  readonly sealed: boolean
  readonly envelope?: Record<string, unknown>
}
```

`mode` is the node's declared effect mode (`hermetic` or `expected`), `tier` is
its declared effect tier (`sealed`, `compensable`, or `irreversible`), and
`onConflict` is its conflict strategy (`serialize`, `lane`, or `fail`).

### 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["input"]["DecodingServices"]>
```

Decodes flow input through the flow's declared input 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: CoreGraph.Graph, options?: FromGraphOptions) => PlanLike
```

Projects the public [`@smthrs/core`](/docs/reference/api/core/) graph introspection API into a
`PlanLike`. Keys come from each node's key 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: CoreGraph.Graph, options?: KeysOptions) => Record<string, string>
```

Derives every node's step key from the graph's digest-free key material.
Sealed material becomes a content key through `StepKey.fromKeyMaterial`, with
dependency references resolved to previously derived keys in topological order.
Non-sealed material becomes a run-local ordinal key, scoped to
`options.runId`, which defaults to `"plan"`.

The compiler is [`@smthrs/plan`](/docs/reference/api/plan/)'s, 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) => Plan
```

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

`Plan` and `Node` are `PlanLike` and `PlanNodeLike` with their
presentation-only collections in canonical order. They are what `make` returns.

### Plan.KeysOptions, Plan.FromGraphOptions, Plan.PlanOfOptions

```ts
interface KeysOptions {
  readonly runId?: string | undefined
}

interface FromGraphOptions {
  readonly key?: ((node: CoreGraph.GraphNode) => string) | undefined
  readonly runId?: string | undefined
  readonly envelope?: Record<string, unknown> | undefined
  readonly digest?: string | undefined
}

interface PlanOfOptions extends FromGraphOptions {
  readonly build?: CoreGraph.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.

### 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 | undefined) => Effect.Effect<void, PlanAssertionError>
  readonly onConflict: (expected: string | undefined) => Effect.Effect<void, PlanAssertionError>
  readonly declaresEffects: (expected: ReadonlyArray<string>) => Effect.Effect<void, PlanAssertionError>
  readonly envelope: (expected: Record<string, unknown> | undefined) => Effect.Effect<void, PlanAssertionError>
}
```

Assertions scoped to a single built node, returned by `expectPlan(plan).node(id)`.

### 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 two instances of the same class, and which reports a cycle
rather than recursing into it. The rendering is total, so no journal value can
throw out of the typed error channel `assertNoDivergence` declares.

### 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: CapabilityExpectation
  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 scratch file the round-trip probe writes, removed
even when the assertion fails. It must not already exist: the suite refuses to
write over a file it did not create, and removes only the file it did. When
omitted, the suite builds a unique 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.

### 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.

### 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 retryAfterMillis?: number | undefined
  readonly resetAtEpochMillis?: number | undefined
  readonly resetSource?: string | 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.

`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: ModelRequestLike
  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.

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.

### 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>
const layerFile: (path: string) => Layer.Layer<FixtureStore>
```

A store over a JSON file. Node only. The file is read once, when the store is
built, and every `append` rewrites it, so a recording run leaves a committable
fixture behind even if a later test in the same run fails. Writes are
serialized: concurrent model calls would otherwise each rewrite the file from
their own snapshot and drop the calls recorded in between.

## 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 on a settled stream and on a provider failure, and stays
silent otherwise. Interruption and a defect both 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
  readonly fixture: FixtureStore
}

const make: (options: Options) => Model.Model
const layer: (options: Options) => Layer.Layer<Model.Model>
```

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.

## 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.Literals<[...]>
```

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`, `task_timeout`, `ralph_max_reached` |

### 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`                     |
| `TaskTimeoutError`            | `task_timeout`                  | `requestId`, `policy` (`fail`), `requestedAtLogicalTimeMillis`, `timedOutAtLogicalTimeMillis`                     |
| `RalphMaxReachedError`        | `ralph_max_reached`             | `loopId`, `maxIterations`                                                                                         |

`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.

`RalphMaxReachedError` reports a bounded loop that ran its whole iteration
budget without its exit condition ever holding. `loopId` names the loop and
`maxIterations` is the bound it reached. A subject whose engine has no loop
runtime never raises it; the code stays in the union so a subject that does can
report exhaustion as a typed failure rather than as a successful final
iteration.

### 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
  | TaskTimeoutError
  | RalphMaxReachedError
  | 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.

## 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.

### 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. A negative pid addresses a 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.

### 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.

### 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. 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` and a bare `new Date()` by `skewMs` for **this process only**.
A child does not inherit it, which is why a child runner takes an explicit skew
instead. `restore` is idempotent.

## 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://time-travel.smithers.sh)

> **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) | SQLite dialect only. Any SQLite-speaking `SqlClient` runs it.                        |

The barrel exports `TimeTravel` and `ReadOnlyTimeTravel` flat, and
the other modules as namespaces. Every module under `src/` is also published at
`@smthrs/time-travel/<Module>` by the package `exports` map, which is where the
members of the `TimeTravel` module other than the service class itself live:
`Position`, `Projection`, `Service`, `Options`, `defaultMaxHistoryEntries`,
`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`, `Retry`, `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.                                                                                                       |

`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                                                                                                                                                                                                         |
| ------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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.                                              |

### ReplayOptions, ForkOptions, RewindOptions

```ts
interface ReplayOptions {
  readonly pageSize?: number | undefined
  readonly maxHistoryEntries?: number | undefined
}

interface ForkOptions {
  readonly workspaceRoot?: string | undefined
  readonly retainWorkspace?: boolean | undefined
  readonly maxHistoryEntries?: number | undefined
}

interface RewindOptions {
  readonly detachedChildren?: "block" | "cancel" | undefined
  readonly pageSize?: number | undefined
  readonly maxHistoryEntries?: number | undefined
}
```

`pageSize` is a throughput knob only and never changes a derived answer; it
defaults to 100. `maxHistoryEntries` overrides `Options.maxHistoryEntries` for
one call. `workspaceRoot` defaults to `.flows/forks` and only moves which lane
the derived workspace name lands in. `detachedChildren` defaults to `"block"`.

### ForkResult and RewindResult

```ts
type ForkResult = TimeTravelStore.Fork

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.                                                                                                                                                                                                                                                        |
| `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. 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, so a retry lands under a fresh
lane name rather than asking Jujutsu for the one the leftover on disk still
holds.

### 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.
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. 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 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.

The one audit it cannot resolve is one whose run a live process still holds.
That one is declined: the audit keeps its `in_progress` status, stays in
`pendingAudits`, and nothing is written, so a later build finishes it. Recording
it `failed` would close it terminally and drop it from `pendingAudits` forever.

`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` counts journal records, so frame `n` means "after the first `n` records
were durable", and `0` is the state before the run wrote anything. `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.                                 |
| `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, warnings }`. A fork's outcome.                                                                 |
| `ForkIntent`    | `{ childRunId, parentRunId, parentSeq, reservedAtMs }`. A minted fork id whose fork has not committed.         |

Three helpers travel with them:

```ts
const auditPatchKeys: ReadonlyArray<string>
const validateAuditPatch: (patch: AuditPatch) => Effect<AuditPatch, TimeTravelError>
const forkFrameMessage: (parentRunId: string, frame: Frame) => string
```

`validateAuditPatch` refuses a patch carrying a key `AuditPatch` does not admit,
because the offending caller is an untyped one. `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.

### 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.                                                                                                                                                                             |
| `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?)` | 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)`                                          | Whether the archive holds a record at that coordinate. Recovery's commit-point evidence.                                                                                                                                                              |
| `nextForkId(parentRunId, frame)`                                  | Mints and durably reserves the run id the next fork off that frame will carry, without creating a run.                                                                                                                                                |
| `abandonForkIntents(staleBeforeMs)`                               | Every reservation older than `staleBeforeMs` whose fork never committed, handed back exactly once.                                                                                                                                                    |
| `createFork(parentRunId, frame, childRunId?)`                     | Branches a new run off a frame, copying the journal prefix and the attempts that existed there, and recording the `fork` edge. The parent is untouched.                                                                                               |
| `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 }
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.

## SqlTimeTravelStore

The durable store, SQLite dialect only.

```ts
const migrate: Effect<void, unknown, SqlClient>
const make: Effect<TimeTravelStore.Service, never, DurableWriter | SqlClient>
const layer: Layer<TimeTravelStore.TimeTravelStore, never, DurableWriter | SqlClient>
```

Building `make` runs `migrate` first, so a fresh database is usable without a
separate setup step. Writes go through `DurableWriter` rather than straight to
`SqlClient`.

## 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 fromEntry: (entry: JournalEvent.Entry) => EffectRecord | undefined
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`.

Prefer `decodeEntry` over `fromEntry`: the first fails closed on a corrupt
payload, the second returns `undefined`.

## 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 same schema as a rung on the shared migration ladder.

| 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 lineage index this set
adds is over the journal's own table, and it ships here because a migrator
refuses any migration whose id sits below the mark the database already applied.

## 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` | The effect already recorded a durable `intended` boundary, so executing it a second time was refused.                                                                                         |
| `rate_limited`        | `rewind`               | The supplied rate limiter 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, or the suffix a fork or rewind 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 and classification, never as the effect's `input` or `output`.

## Limits

- The durable store is SQLite dialect only. Its DDL uses `typeof()` and
  `json_valid` CHECK constraints, its reads use `json_extract` with `$` paths,
  and its archive writes use `INSERT OR IGNORE`, so any SQLite-speaking
  `SqlClient` runs it and nothing else does. PostgreSQL and PGlite are
  unsupported.
- 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.
- `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://smithers.sh/docs/reference/cli/). 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`, 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 same DDL as a rung on the shared ladder at
id block `5000`, for a composition that owns migration itself.

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.

---

# @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://triggers.smithers.sh)

> **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/).

The package is not on npm at 1.0.0-rc.0. See [Installation](https://triggers.smithers.sh/installation/)
for how to get it.

## 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.

`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.

## 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, so a retry cannot fire it
twice.

`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"

interface Result extends Fire {
  readonly outcome: Outcome
  readonly runId?: string | undefined
  readonly error?: string | undefined
}
```

### 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<Registered>, 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 setPending: (fire: Fire) => Effect.Effect<void, TriggerError>
  readonly takePending: (triggerId: string) => Effect.Effect<Option.Option<number>, 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 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.                                                                                                                                                                  |
| `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.                                                                                                              |
| `setPending`       | Buffers an occurrence, coalescing with any already pending.                                                                                                                                    |
| `takePending`      | Removes and returns the buffered occurrence.                                                                                                                                                   |
| `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"`.          |
| `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.                                         |

Every method addressing one trigger fails with `unknown_trigger` when no such
row exists, except `clearActive`, whose compare-and-swap cannot tell a missing
trigger from a run id that no longer matches and so stays a no-op for both, and
`history`, `heartbeat`, and `lastHeartbeat`, which address the whole store.

### TriggerStore.FireRecord, HistoryQuery, HistoryPage, Held, 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 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.

### 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) => string
const isReservation: (runId: string | undefined) => boolean
const reservationOccurrence: (runId: string) => number | undefined
```

`reservationPrefix` is `trigger-reservation:`, and `reservationId` appends the
trigger id and the occurrence. `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`.

### SqlTriggerStore.reservationLeaseMs

Re-exported from `TriggerStore`, which owns the value.

## 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
}

interface RunnerService {
  readonly start: (input: StartInput) => Effect.Effect<string, TriggerError>
  readonly isActive: (runId: string) => Effect.Effect<boolean, TriggerError>
  readonly cancel: (runId: string) => Effect.Effect<void, TriggerError>
}
```

`idempotencyKey` is `<triggerId>:<occurrence ISO instant>`, so two hosts that
notice the same boundary derive the same key.

### 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, `isActive` with `false`, 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.

Liveness is read as the complement of the settled statuses `cancelled`,
`completed`, and `failed`, 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 host?: string | undefined
}

interface Service {
  readonly runOnce: Effect.Effect<void, TriggerError>
}

const defaultHost: string
```

`pollInterval` defaults to one minute and paces the tick loop.
`runPollInterval` defaults to one second 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.

`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.

`runOnce` holds a semaphore permit, so concurrent calls on one scheduler
serialize.

### 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.

## 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` and answers every matching row 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.

### 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.

## 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 returns the same refusal codes in the same order as the SQL store
and holds the same reservation lease, so a test that swaps one for the other is
testing the protocol rather than the implementation.

## 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. |
| `trigger_disabled`        | A claim reads a disabled trigger inside its transaction.                                                     |
| `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 SQL registration receives input with no JSON representation.      |
| `invalid_options`         | A cron occurrence limit, a history page limit, or a scheduler polling interval 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.                    |
| `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://triggers.smithers.sh/troubleshooting/).

## 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 and occurrence;
`TriggerStore.reservationOccurrence` reads that occurrence back. The
`SqlTriggerStore.reservationLeaseMs` lease is 300,000 milliseconds, or 5
minutes. `TriggerStore.reservationLeaseMs` owns the shared value and
`SqlTriggerStore` re-exports it. 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.

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://triggers.smithers.sh/concepts/claim-protocol/).

## 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` and `runPollInterval` must be finite, positive
Effect durations. 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 the declaration cannot honor is a statement about how much history to
replay, not a reason to stop scheduling: the scheduler logs a warning annotated
with the trigger id, abandons the backlog beyond the bound, and still fires the
current occurrence.

## Webhook verification and input ownership

The signature verifier looks up `SignatureConfig.header` in `RawInbound.headers`
first as lowercase and then exactly as written. 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. This step also
copies a `SharedArrayBuffer`-backed view 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`, and `0003_heartbeat` in order. The package exports
`@smthrs/triggers/package.json`. It does not export `internal/*` or nested
`*/index` subpaths.

---

# Environment variables
https://smithers.sh/docs/reference/environment-variables/

These variables configure the CLI's control, cache, and provider integrations. An empty control-plane setting reads like an unset value. Individual build rules and external tools can read additional variables declared by their own contracts.

## Operator variables

Variables an operator exports to steer the CLI.

| Name | Set by | Purpose | CLI flag equivalent |
| --- | --- | --- | --- |
| `SMITHERS_AUDIENCE` | operator | Presentation audience: `auto`, `human`, or `agent`; does not change authority. | `--audience` |
| `SMITHERS_CREDENTIAL_KEY` | operator | Base64-encoded 32-byte host key for encrypting stored credentials. Keep it separate from the database. | none |
| `SMITHERS_CACHE_URL` | operator | Remote target-cache endpoint. | none |
| `SMITHERS_CACHE_TOKEN` | operator | Remote target-cache credential. | none |
| `SMITHERS_CACHE_NAMESPACE` | operator | Trust domain used when publishing target-cache results. | none |
| `SMITHERS_REMOTE` | operator | Control-plane base URL. | `--remote` |
| `SMITHERS_API_KEY` | operator | Preferred bearer credential channel for the remote control plane; avoids argv exposure. | `--credential` |
| `SMITHERS_MCP_CONFIG` | operator | Path to the MCP server array the local executor projects into a run's flow catalog. | `--mcp-config` |
| `SMITHERS_OPENAI_AUTH` | operator | `api-key` or `chatgpt`, selecting how `openai` seats authenticate. `chatgpt` reads the codex CLI's ChatGPT session at `$CODEX_HOME/auth.json` instead of `OPENAI_API_KEY`; any other value refuses the seat. | none |
| `SMITHERS_BACKEND` | operator | Database backend; only `sqlite` is supported. Any other value exits 1 with `unsupported_database`. | `--backend` (hidden) |
| `SMITHERS_BUG_ENDPOINT` | operator | Where `smthrs bug` posts its report. | none |
| `SMITHERS_JJ_PATH` | operator | Explicit path to the `jj` binary. | none |
| `SMITHERS_DETACHED_ADMISSION_TIMEOUT_MS` | operator | How long a detached flow launch waits for the detached run's admission line. A value that is not a positive integer is ignored. | none |
| `SMITHERS_TEST_COMMAND` | operator | The command the `test` flow runs. | none |
| `SMITHERS_TEST_CONTAINER` | operator | The container the `test` flow runs in. | none |
| `SMITHERS_TEST_CWD` | operator | The repository's path inside that container. | none |
| `SMITHERS_TEST_TIMEOUT_MS` | operator | Wall-clock budget for one `test` invocation. | none |

## Variables the engine sets

Set by the engine on the agent processes it spawns; never set them by hand.

| Name | Set by | Purpose | CLI flag equivalent |
| --- | --- | --- | --- |
| `SMITHERS_INSIDE_RUN` | engine | Marks a process as an agent process inside a run. | none |
| `SMITHERS_RUN_ID` | engine | The run an agent process belongs to. | none |

## Provider keys

Provider keys are read by the seat resolver, not by the CLI itself; `smthrs doctor` reports which are present.

| Name | Read by | Purpose |
| --- | --- | --- |
| `ANTHROPIC_API_KEY` | seat resolver | Credentials `anthropic:*` seats. |
| `OPENAI_API_KEY` | seat resolver | Credentials `openai:*` seats when `SMITHERS_OPENAI_AUTH` is unset or `api-key`. |
| `OPENROUTER_API_KEY` | seat resolver | Credentials `openrouter:*` seats. |
| `CEREBRAS_API_KEY` | seat resolver | Credentials for `cerebras:*` seats. |
| `MOONSHOT_API_KEY` | seat resolver | Credentials for `moonshot:*` seats. |
| `GEMINI_API_KEY` | seat resolver | First credential source for `gemini:*` seats. |
| `GOOGLE_API_KEY` | seat resolver | Fallback for `gemini:*` seats when `GEMINI_API_KEY` is unset or empty. |

## Removed in 1.0

`SMITHERS_HOME`, `SMITHERS_TOKEN`, and the `SMITHERS_WORKFLOW_*` family are gone: `~/.smithers` is not a runtime input, and `SMITHERS_TOKEN` belongs to a different product's CLI. See [the 1.0 migration guide](/docs/migration/1.0/).

---

# Exit codes and error codes
https://smithers.sh/docs/reference/errors/

## Exit codes

`smthrs` uses one status vocabulary, so a script can branch on it.

| Code | Meaning |
| --- | --- |
| `0` | The command did what it was asked. |
| `1` | The command failed, or the run it reports settled `failed`. |
| `2` | The invocation was wrong. Retype the command; the message names the flag or argument. |
| `3` | The run is parked at `waiting-approval`. Answer it with `smthrs approve` and resume. |
| `130` | The run was cancelled or interrupted. |
| `143` | The run was terminated. |

Codes 3, 130, and 143 report a run outcome rather than a failure of the command, and are decided from the control receipt alone. Of the typed CLI failures, a `UsageError` exits 2 and every other (`UnsupportedError`, `ResourceLimitError`, `RenderingError`) exits 1. A removed 0.x verb or flag exits 1 with a message pointing at `https://smithers.sh/migration/1.0`.

## Structured refusal codes

Each code below is stable and safe to branch on, grouped by the package that raises it. Symptom-first entries live in [Troubleshooting](/docs/troubleshooting/).

### The `smthrs` CLI

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `unsupported_database` | CLI startup, when `SMITHERS_BACKEND` or `--backend` has any value but `sqlite` | 1.0.0-rc.0 supports local SQLite only; PostgreSQL and PGlite are not available | Unset `SMITHERS_BACKEND` or set it to `sqlite`; exits 1. See [Unsupported database](/docs/troubleshooting/unsupported-database/) |

Exported 0.x connection strings (`SMITHERS_TEST_PG_URL`, every `SMITHERS_POSTGRES_*` name) are a separate case: each gets one notice line, `ignored: <name> has no effect in 1.0.0-rc.0 (SQLite only)`, and moves no exit code.

### `@smthrs/database`

Raised as defects by `NodeDatabase.layer` before a connection is created; match them with `isUnsupportedDatabase`.

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `unsupported_runtime` | `NodeDatabase.layer` | The durable engine was opened under Bun (`process.versions.bun` is set) | Run the durable engine on Node.js 22.19.0 or later |
| `unsupported_database_file` | `NodeDatabase.layer` | The file has at least one table and no `flows_migrations` table | The file is a 0.x `smithers.db`, which 1.0.0-rc.0 does not load; open a 1.0 database |
| `database_locked` | `NodeDatabase.layer` | A peer held the file for the whole open ladder, so it was never inspected | Stop the process holding the file and open it again |

### `@smthrs/flow`

#### Sleep (`SleepRequestInvalid`)

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `missing_deadline` | `Sleep` | The payload named neither `millis` nor `until` | Name exactly one deadline |
| `ambiguous_deadline` | `Sleep` | The payload named both, the same fact stated two ways | Name one |
| `invalid_deadline` | `Sleep` | The value is not a length of time: a non-finite or negative `millis`, a non-finite `until`, or an addition that overflows | Pass a finite, non-negative deadline |

#### WaitFor (`WaitForRequestInvalid`)

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `missing_target` | `WaitFor` | The payload named neither `name` nor `token` | Name one wait point |
| `ambiguous_target` | `WaitFor` | The payload named both | Name one |
| `malformed_token` | `WaitFor` | The token does not parse | Pass the branded token the deferred minted |
| `foreign_execution` | `WaitFor` | The token is addressed to another flow or another execution | Complete waits with a token of the same flow and execution |

#### Poll (`PollExhausted`)

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `poll_exhausted` | `Poll` | The attempt bound was reached with `onTimeout: "fail"` | Raise `maxAttempts`, or answer with the last check output through `onTimeout: "return-last"` |

#### HumanTask (`HumanTaskFailed`)

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `request_invalid` | `HumanTask` | An unanswerable question: a `select` with no options, an attempt budget out of range, a `timeoutMs` that is not a length of time, a schema outside the bounded subset, or a schema supplied with the wrong `kind` | Fix the question; it is refused before anyone is asked |
| `rejected` | `HumanTask` | The attempt budget was spent on answers the task refused | Read the carried `rejections` and answer within the declared kind and schema |
| `timeout` | `HumanTask` | The deadline passed with the question open | Answer within `timeoutMs`, or raise it |

#### Retry policy

An exhausted or expired action retry policy propagates the final declared failure through the action's error channel. Typed recovery can handle that failure. The action span records `retry.stopReason` as `"exhausted"` or `"expired"` and `retry.attempt` as the one-based attempt. Inspect these annotations before adjusting `maxAttempts` or `expirationMs`. See [Retries](/docs/concepts/retries/).

#### Interpreter (`InterpreterError`)

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `incomplete_graph` | The body interpreter | The build reported the topology | Fix the body so the graph builds without diagnostics |
| `duplicate_node_id` | The body interpreter | Two nodes answer to one dispatch address | Give each node a distinct id |
| `unresolved_action` | The body interpreter | An action has no implementation wired up | Provide the action's `toLayer` |
| `unresolved_reference` | The body interpreter | A payload reads a node this graph does not hold | Reference only nodes the graph holds |
| `unsupported_call` | The body interpreter | A call's declaration did not survive serialization | Declare the call so it round-trips |
| `missing_operation` | The body interpreter | A deferred function did not survive serialization | Declare the function so it round-trips |

#### Cancellation (`CancelRequestFailed`)

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `cancel_request_failed` | `FlowRuntime` cancellation | A durable runtime could not record a cancellation (a storage failure) | Retry the cancellation against healthy storage |
| `unsafe_interrupt_unsupported` | `FlowRuntime.interruptUnsafe` | The durable engine does not implement unsafe interruption | Use the supported interruption path instead of `interruptUnsafe` |

### `@smthrs/journal`

`JournalError.code` is stable. The two channels (lossy and durable) fail independently, and neither failure is permanent.

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `invalid_event` | Journal writes and reads | A contract violation in the caller's own input: a malformed identifier, a missing or malformed owner, or an event over an opted-in byte bound | Fix the submitted event |
| `idempotency_conflict` | Emission and `flush` | A reused producer sequence carrying different content | Retry with identical content (a `Duplicate` receipt) or under a fresh `sourceSeq` |
| `sequence_conflict` | Journal writes | An identity collision on allocation | See the [`@smthrs/journal` reference](/docs/reference/api/journal/) |
| `fence_lost` | Fenced durable writes, checkpoints, compactions | The run's recorded owner moved; this writer no longer owns the run | Stop writing: the run belongs to its recorded owner |
| `queue_overflow` | `emitLossy` | The bounded admission queue is full | Raise `capacity` or shed telemetry; lifecycle writes use `emitDurable`, never this queue |
| `journal_closed` | Admissions | The journal is closed | Do not emit after the layer closes |
| `sink_failed` | The durable write path | A database failure on write | Restore the database; the writer fiber survives |
| `read_failed` | The read path | A database failure on read | Restore the database and read again |
| `decode_failed` | Reads | A row no longer matches the schema | Inspect the stored row; the journal refuses to decode it |
| `checkpoint_invalid` | `checkpoint` and `compact` | A missing or invalid checkpoint or sequence; `checkpointSeq` is optional | Write a checkpoint at a committed sequence above the compaction floor, or select an existing valid checkpoint for compaction, then retry |
| `reader_behind` | `compact` | A live stream still needs entries below the requested checkpoint; carries `checkpointSeq` | Let the blocking stream catch up or close it, then retry compaction |
| `compacted` | Reads below a run's compaction floor | The entries below the floor were deleted; carries the floor as `checkpointSeq` | Load `latestCheckpoint(runId)`, apply its `state`, then resume the tail with `afterSequence: checkpoint.seq` |
| `unknown` | Any journal path | A genuinely unclassified journal defect | Treat it as a defect, not a caller error |

### `@smthrs/control`

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `plan_not_found` | `run`, `approve`, `deny` (`PlanNotFound`) | The named plan does not exist | Create the plan with `smthrs plan`; the carried `planId` names the plan to create or replace |
| `plan_denied` | `run` (`PlanDenied`) | The plan was denied | Plan again; a denied plan cannot run, and the carried `planId` names the plan to replace |

### CLI rendering (`RenderingError`)

Caller-controlled output was not inert bounded data; rendering failures exit 1.

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `accessor`, `byte_limit`, `callable`, `cycle`, `depth_limit`, `member_limit`, `proxy`, `to_json`, `unsupported`, `unreadable` | The CLI output renderer | The rendered value was not inert bounded data; the code names the kind and the path identifies the first refusing member | Render plain bounded data; the renderer accepts at most 128 nested levels, 10,000 data members, and 4 MiB of UTF-8 output |

### `@smthrs/errors` (`SmithersError`)

The five codes carried by `SmithersError`, the class the integration adapters
raise. Every one of these is what a `SmithersError.message` points at, so this
is the page a user reaches from a log line. Branch on `error.code`; the union
is closed, so a `switch` over it stays exhaustive. The complete reference,
with every raise site by function name, is on the
[package's own site](https://errors.smithers.sh/reference/error-codes/).

| Code | Where raised | Cause | Fix |
| --- | --- | --- | --- |
| `INVALID_INPUT` | Integration helpers | An argument the helper cannot use: a missing bot token, an approval option key containing a colon, callback data over Telegram's 64-byte limit, or a non-https Mini App URL | Correct the argument; the carried field names it |
| `INTEGRATION_ERROR` | Integration clients, webhook sources, listener reconciliation | A provider call or reconciliation failed; `reason` classifies it so a caller can map it to a transport status | Branch on `details.reason`; retry only the reasons that are transient |
| `TELEGRAM_API_ERROR` | The Telegram Bot API client | The API answered `ok: false`, returned a non-JSON body, or the transport failed | Read `details.errorCode` and `details.description`; honour `details.retryAfterSeconds` when present |
| `TELEGRAM_INIT_DATA_INVALID` | Mini App `initData` verification | The payload is empty, expired, missing its hash or signature, or failed HMAC or Ed25519 verification | Re-request `initData` from the client; the expiry failures carry `authDate` |
| `UNSUPPORTED` | Any adapter needing a runtime primitive | The runtime lacks something the adapter needs, such as Web Crypto or Ed25519 verification | Run on a runtime that provides the primitive |

The bot token is redacted from both the message and the details of every
`TELEGRAM_API_ERROR`. Redaction happens where the error is raised, not where
it is logged.

---

# flow.mdx frontmatter
https://smithers.sh/docs/reference/flow-mdx/

A markdown flow's YAML frontmatter configures the run; the body below the closing fence is the agent's instructions.

## Keys

The parser accepts exactly these keys, in the order the run reads them.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | string | no | the directory path below `flows/` | A project's flows take their id from the directory path, so a `name` key is ignored with a `name_field_ignored` discovery warning. |
| `description` | string | yes | none | One line describing the flow. A flow with a missing or empty description is not discovered; one over 1024 characters earns an `invalid_description` warning. |
| `model` | string | no | none | The model seat the flow runs on, for example `openai:gpt-5.6-sol`. An empty value reads as absent, and `smthrs up` cannot launch a prompt flow that declares no seat. |
| `capabilities` | string array | no | `["*"]` | The capability patterns that bound what the run may do. A space-separated string is accepted with a warning; an absent or malformed value grants the all-action wildcard with a warning. Declare only the patterns the task requires. |
| `budget` | object | no | none (unbounded) | The ceilings a control plane approves for one run: `tokens` and `milliseconds`, each a positive safe integer. An absent key is no ceiling, not zero; a malformed budget is dropped with a warning rather than tightened. |
| `flows` | string array | no | none | Flow ids this flow may delegate to. A space-separated string is also accepted. When both spellings are present, `flows` wins over `allowed-tools`. |
| `allowed-tools` | string or string array | no | none | The Agent Skills spelling of `flows`. |
| `effects` | object | no | inferred from `capabilities` | Declared effect boundaries: `reads` and `writes` string arrays, `tier` (`sealed`, `compensable`, or `irreversible`), `mode` (`hermetic` or `expected`), and `onConflict` (`serialize`, `lane`, or `fail`). A wildcard capability yields conservative `**` reads and writes. |
| `placement` | string | no | none | Where the flow runs: `client`, `local`, `sandbox`, or `remote`. Any other value is ignored with a warning. |
| `effort` | string | no | the host default, `high` | The reasoning effort the run's model calls request: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. An unrecognized spelling falls through to the default rather than failing the launch. |
| `disable-model-invocation` | boolean | no | `false` | When `true`, the flow is not model-invocable. The strings `"true"` and `"false"` are also accepted; anything else is ignored with a warning. |
| `license` | string | no | none | License declaration; a non-string value earns an `invalid_license` warning. |
| `compatibility` | string | no | none | Compatibility note of at most 500 characters. |
| `metadata` | object | no | none | A string-to-string mapping; any other shape earns an `invalid_metadata` warning. |
| `input` | any | no | none | Recognized but unsupported in markdown flows; ignored with an `unsupported_input_schema` warning. A markdown flow's input is always `{ args: string }`. |
| `schema` | any | no | none | Recognized but unsupported in markdown flows; ignored with an `unsupported_input_schema` warning. A markdown flow's output is always a string. |

Any other key earns an `unknown_frontmatter_key` discovery warning, and discovery warnings surface in `smthrs doctor`.

## A complete example

The frontmatter of `flows/review/flow.mdx`, with the first body heading that follows it:

```md
---
description: Reviews the uncommitted change in this repository and returns a verdict with the reasons behind it.
capabilities: ["fs:read:**", "proc:spawn:*"]
model: openai:gpt-5.6-sol
budget:
  tokens: 200000
  milliseconds: 600000
---

# Review the working-copy change
```

## The body

The markdown below the closing `---` fence is the prompt the agent is handed, and every line of it is an instruction. Headings organize that prompt; they do not create separate durable steps or enforce execution order. Arguments passed at launch are appended after the body.

## Featured flows

Frontmatter describes the flow. It never says how the repository presents it: the one-line `summary` a listing shows under the id, and whether the flow is `featured`, one of the handful the repository recommends first. Those two keys are not frontmatter keys; a `summary` or `featured` key in `flow.mdx` earns an `unknown_frontmatter_key` warning like any other. They are declared once, in `.smithers/FACTORY.ts`, with `Smithers.Flow` under the factory's `flows`:

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

export const review = S.Flow({
  flow: "review",
  summary: "Review the working-copy change and return a verdict with the reasons behind it.",
  featured: true
})

export const factory = S.Factory({
  summary: "How this repository develops itself.",
  flows: [review]
})
```

```ts
// PACKAGE.ts
import { Smithers } from "@smthrs/targets"

const factoryProjection = Smithers.FactoryProjection({})
```

`flow` is the flow id, the directory path below `flows/`. `summary` is one non-empty line. `featured` defaults to `false`.

`Smithers.FactoryProjection` is the target that joins these declarations with discovery and writes the `flows` rows of `.smithers/factory.json`: one row per discovered flow with `id`, `description`, `summary`, `featured`, `kind` (`ts`, `mdx`, or `skill`), `path`, `capabilities`, `model`, and `modelInvocable`, featured rows first in declaration order. The file is checked in, so smithers.sh and a workspace without `node_modules` read the featured set without running discovery. The planner fills the target with the loaded `FACTORY.ts`; the `PACKAGE.ts` never restates the declaration.

- `smithers-build target //:factoryProjection --write` regenerates the file, beside `.smithers/home.json` when the factory exports a `home`.
- `smithers-build ci //:factoryProjection` fails on drift, the same way the generated `ci.yml` does.
- A declaration whose `flow` names no discovered flow fails the write with the id in the message. A recommendation for a flow that does not exist is never written.
- `smthrs ls` reads the projection when it is present and marks featured flows with a leading `*` in its human output.

## See also

- [Markdown flows](/docs/guides/markdown-flows/) walks through authoring one.
- [Capabilities](/docs/guides/capabilities/) explains the patterns `capabilities` accepts.
- [Project layout](/docs/reference/project-layout/) shows where `flow.mdx` sits.

---

# Glossary
https://smithers.sh/docs/reference/glossary/

One sentence per term, using the same names the prose uses.

| Term | Definition |
| --- | --- |
| [action](/docs/concepts/flows-actions-plans/) | A unit of work with a tag and schemas; its implementation is supplied by a layer. |
| approval | The recorded decision that accepts a plan digest or answers a parked run's ask. |
| approval payload | The complete serialized value a decision binds to: the `approval` field of a plan card, or the `payload` of a parked run's `control.approval.requested` event. |
| [attempt](/docs/concepts/retries/) | One numbered try of a step, persisted so the retry budget survives restarts; `CurrentAttempt` exposes the one-based count. |
| [body](/docs/concepts/flows-actions-plans/) | A flow's required pure function, which records nodes that stand for work instead of performing it. |
| [capability](/docs/concepts/kernel/) | One member of the closed `Capability.Action` vocabulary (such as `fs:read`, `net:get`, or `model:call`), carrying the exact resource a grant authorizes. |
| [cell](/docs/concepts/agent-runtime/) | The JavaScript program the model emits for one frame, evaluated in a persistent QuickJS realm whose only authority is `ctx.call`. |
| [compensation](/docs/concepts/time-travel/) | The undo of an effect inside its declared boundary, performed by registered handlers when a rewind crosses the record. |
| [content addressing](/docs/concepts/content-addressing/) | Deriving identifiers from canonical content; action tags and other declared key material can affect identity. |
| control plane | The `Control` service and everything serving it: `plan`, `run`, `approve`, `deny`, `steer`, `signal`, `cancel`, `resume`, `list`, and `watch` over durable runs. |
| deferred | A durable wait point (`DurableDeferred`) that parks a run until another process completes it by token, first writer wins. |
| detached run | A run launched with `smthrs flow start -d` in a child process that outlives the launcher, reported admitted once its run row is durable. |
| diagnosis card | The read-only card `smthrs runs show <run-id>` prints: verdict, run, activity, tokens, refusals, and the next command, computed from the run's journaled events. |
| [durable execution](/docs/concepts/durable-execution/) | Recording action outcomes and execution state so a later process can reuse completed work and resume pending work. |
| [engine store](/docs/concepts/durable-execution/) | The durable engine (`@smthrs/engine-store`): the store that claims, fences, and persists runs over the journal in SQLite. |
| [execution ID](/docs/concepts/ownership/) | The identity of a run, chosen by the caller on `execute`, else derived from the flow's `idempotencyKey`, else from the ambient source. |
| [fence](/docs/concepts/ownership/) | The ownership check that lets a write land only while the store still records the writer as the run's owner, failing `fence_lost` otherwise. |
| [flow](/docs/concepts/flows-actions-plans/) | A declaration with payload and result schemas and a body that composes actions and child flows. |
| [follower](/docs/concepts/sync/) | A read-only sync client that receives journal rows verbatim and holds no authority: no lease, no fence, no dispatcher. |
| [frame](/docs/concepts/time-travel/) | In time travel, a `{ lineageId, seq }` address naming the committed prefix of one journal lineage; in the agent runtime, one cycle of the cell loop (model, cell, evaluation, durable calls, transition). |
| [grant](/docs/concepts/kernel/) | A recorded permission decision (`once`, `run`, `remembered`, or `deny`), persisted before it activates. |
| [host](/docs/concepts/kernel/) | The closed surface of platform services a flow may reach (`FileSystem`, `HttpClient`, `CommandLine`, `ChildProcessSpawner`, `Workspace`, `Jj`), each grant-checked by the kernel. |
| [journal](/docs/concepts/durable-execution/) | The logical write-ahead log over SQLite that records one run's history in order and is the run's primary state. |
| [layer](/docs/concepts/why-effect/) | The Effect unit that attaches an implementation to a declaration and composes services, replacing any hook list or global registry. |
| [memory](/docs/concepts/memory/) | Durable cross-run storage for facts, history, and notes under namespaces, with replaceable recall strategies. |
| [namespace](/docs/concepts/memory/) | The lifetime scope of a memory record: a kind (`flow`, `agent`, `user`, or `global`) plus an id, spelled publicly as a bank. |
| [node](/docs/concepts/flows-actions-plans/) | One keyed member of a plan graph standing for a dispatch; a node id is durable dispatch identity. |
| parked | Stopped at a durable wait: the run row records a waiting reason and the run re-drives when the matching event lands. |
| [payload](/docs/concepts/flows-actions-plans/) | The schema-typed input of a flow or action, stored as its inert JSON mirror. |
| [plan](/docs/concepts/flows-actions-plans/) | The action graph for a planning round, identified by digest; later rounds can depend on earlier results. |
| projection | A read model folded from a run's ordered events, served as snapshots and subscriptions; subscribing never claims a run and never writes. |
| receipt | The value every control mutation answers (`Accepted`, `AlreadyApplied`, `Conflict`, `Parked`, or `Terminal`); `plan` answers a `PlanCard` instead. |
| registry | The project's flow discovery service: the descriptor catalog `smthrs flow list` lists and flow ids come from. |
| [replay](/docs/concepts/durable-execution/) | Reading recorded history without repeating work; during execution recovery, recorded outcomes also allow the driver to skip completed actions. |
| [resume](/docs/concepts/durable-execution/) | Re-driving a parked or reclaimed run from its durable state, the same operation a restart and `smthrs runs resume` perform. |
| [retry policy](/docs/concepts/retries/) | A frozen plain value whose next delay derives from a persisted attempt count, with `maxAttempts` and `expirationMs` bounding it. |
| [run](/docs/concepts/durable-execution/) | One durable execution of a flow, addressed by its run id, with its own journal partition, an owner, and a status. |
| [sandbox](/docs/concepts/agent-runtime/) | The isolated evaluator a cell runs in (a persistent QuickJS realm per run, bounded by `Sandbox.Limits`); `@smthrs/sandbox` is also the package of provisioned machines for remote process execution. |
| [seat](/docs/concepts/agent-runtime/) | A model assignment in two halves: the declared string (such as `anthropic:claude-sonnet-4-5`) and the resolved credentialed model a `SeatResolver` returns. |
| signal | A durable JSON delivery to a run's named wait point, made with `smthrs runs signal` or `Control.signal`. |
| [step](/docs/concepts/durable-execution/) | A unit of work whose outcome is recorded for a run; external side effects are not made atomic with that record. |
| [step key](/docs/concepts/content-addressing/) | A `key1_`-prefixed SHA-256 derived from canonical key material, including the action identity and inputs. |
| steering | A durable, attributed message to a live run (a body, a seat, a thinking level, or a tool set) that the next turn picks up. |
| suspended | The result a flow settles with when it parks on a durable wait and will be re-driven. |
| [sync](/docs/concepts/sync/) | Read-only journal replication: `Sync.Read` pages entries and `Sync.Subscribe` streams them to followers under a credit bound. |
| [time travel](/docs/concepts/time-travel/) | The `@smthrs/time-travel` protocols (replay, fork, rewind, compensation, recovery) addressed by a `Position` over the journal a run wrote. |
| token (deferred) | The branded string a `DurableDeferred` issues, encoding the flow name, execution id, and deferred name so another process completes the correct durable address. |
| waiting-approval | The status of a run parked on an approval ask; the CLI reports it with exit code 3, and the answer is an approve or deny decision on the ask's payload. |
| [workspace](/docs/concepts/sync/) | One project's runs and state as a whole: the unit a gateway serves, a workspace subscription follows, and `GET /health` identifies by hash. |

---

# 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_API_KEY` (preferred) or passed with `--credential`. Without one the verb refuses: `Refusing to bind <host> without a bearer token: set SMITHERS_API_KEY (preferred) or pass --credential.`

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

When the web Worker's `GATEWAY_UPSTREAM_URL` is configured, its gateway relay
requires a validated, allowlisted session before attaching the deployment's
`GATEWAY_AUTH_TOKEN` or configured session identity. This includes HTTP RPC
and WebSocket upgrades. A missing or invalid session receives 401; an account
outside the allowlist receives 403. If `IDENTITY_UPSTREAM_URL` is unset, the
relay fails closed with 501.

Only ordinary `GET` and `HEAD` requests to the exact `/health` mount remain
anonymous. A WebSocket upgrade at that path still requires a session.

## 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: the frozen 1.0.0-rc.0 contract removed it. `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.

`SyncRpcs` on `/sync` is read-only journal replication: `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 at 1.0.0-rc.0, 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.

---

# llms.txt
https://smithers.sh/docs/reference/llms-txt/

Smithers publishes two files for agents at the site root, following the [llms.txt convention](https://llmstxt.org/):

- [`/llms.txt`](/llms.txt) is the curated index: what Smithers is, then every docs page as a link with a one-line description, grouped by section. An `## Optional` section holds the example programs an agent may skip on a first pass.
- [`/llms-full.txt`](/llms-full.txt) is the full prose of every docs page in one file, each section headed by its title and canonical URL.

## Point an agent at them

Paste the URL into the conversation, or have the agent fetch it:

```text
Read https://smithers.sh/llms.txt and use it to answer questions about
Smithers. Fetch the pages you cite.
```

Use `/llms.txt` when the agent should navigate and fetch only what it needs. Use `/llms-full.txt` when the agent should hold the whole documentation at once, for example when you are writing flows offline.

## How agents read these docs

Every page is written to stand alone when retrieved in isolation: code samples name their file and language, expected output appears verbatim where the source pins it, and every entry names itself fully (for example `retry.maxAttempts`, not "maxAttempts" under a Retry heading). An agent quoting a page to you should not need the pages around it.

For driving Smithers itself from an agent, the [Smithers MCP server](/docs/guides/mcp-setup/) is the direct path, and the [MCP tools reference](/docs/reference/mcp-tools/) lists every tool it exposes.

## How the files stay current

Both files are generated from the docs content tree by `apps/site/scripts/generate-llms.mjs` and committed. Re-run the script after editing docs pages:

```bash
node apps/site/scripts/sync-api-docs.mjs
node apps/site/scripts/generate-llms.mjs
```

`node apps/site/scripts/generate-llms.mjs --check` fails without writing when the committed files drift from the content tree.

---

# MCP tools
https://smithers.sh/docs/reference/mcp-tools/

`smthrs --mcp` exposes the canonical CLI command tree through a stdio MCP server. The default discovery interface lists capabilities first and loads each command's schema when requested.

The command definitions are the source of truth for both CLI arguments and MCP inputs. Inspect a tool's schema before calling it; do not infer field names from its shell spelling.

## Discovery tools

The server exposes these entry points:

| Tool | Input | Result |
| --- | --- | --- |
| `search_tools` | `query` (default empty), `limit` (1 to 20, default 5), `offset` (nonnegative, default 0) | Matching names, descriptions, annotations, and `nextOffset` when another page exists. |
| `get_tool_details` | `name`, the exact discovered tool name | The input schema, optional output schema, annotations, and instructions. |
| `call_read_tool` | `name` and `arguments` (an object, default empty) | Executes a tool explicitly marked read-only. |
| `call_write_tool` | `name` and `arguments` (an object, default empty) | Executes a writable or unclassified tool. |

An empty search query pages through all available tools. Use the returned `nextOffset` to continue.

## Discover, inspect, call

Search for a capability with a `search_tools` call:

```json
{"query":"flow","limit":5}
```

Inspect a result with `get_tool_details`:

```json
{"name":"flow_list"}
```

Then call it with the arguments its schema accepts. For a project at `/work/project`, the current flow catalog command accepts:

```json
{"name":"flow_list","arguments":{"root":"/work/project"}}
```

Replace `/work/project` with the absolute project path. `flow_list` is currently unclassified, so it uses `call_write_tool` even though listing flows does not execute them. Choose the wrapper from the returned `readOnlyHint`, not from a guess about the command name.

## Command names

Nested CLI commands appear with underscore-separated names. The available schemas determine the exact inputs and defaults.

| Task | Discover this tool |
| --- | --- |
| List or plan flows | `flow_list`, `flow_plan` |
| Plan, approve, and launch | `flow_start` |
| Execute a separately approved payload | `flow_execute` |
| Inspect a run or its output | `runs_show`, `runs_logs`, `runs_output` |
| Resume or cancel a run | `runs_resume`, `runs_cancel` |
| Inspect or change historical state | `runs_inspect`, `runs_replay`, `runs_fork`, `runs_rewind` |
| Read and answer approvals | `approvals_list`, `approvals_approve`, `approvals_deny` |
| Inspect target declarations | `targets` |
| Operate agent state | Search for `memory`, `credentials`, `triggers`, or `eval`. |

The long-running `serve` command is excluded from MCP, as is `eval run`, which imports and executes a local project module. Launch the control-plane server and evaluation runs from a shell.

## Errors and execution boundaries

Discovery of an unknown name returns an MCP tool error. Calling a tool with the wrong read/write wrapper also fails. Invalid arguments are checked against the command schema before its handler executes.

A tool invocation runs the underlying command. `flow_start` grants run-scope plan approval and launches work, just as the CLI does. Tool discovery and schema inspection do not grant permission to execute that work.

Historical mutations retain their CLI requirements. A rewind needs an exact frame and either a preview or explicit confirmation. Remote connectivity is command-specific: history and local operator commands require the host that owns the state.

## Compatibility

The older semantic server described tools such as `list_workflows` and `run_workflow`, along with `semantic`, `raw`, and `both` surfaces. Those names and flags do not describe the default unified `smthrs --mcp` entry point. Re-discover tools when moving an existing agent integration to the unified CLI.

For client setup, see [Drive Smithers from your agent](/docs/guides/mcp-setup/). For the command groups, see the [CLI reference](/docs/reference/cli/).

---

# Project layout
https://smithers.sh/docs/reference/project-layout/

A Smithers workspace holds target declarations and flow sources. Runtime state lives separately under `.flows/`.

## The tree

- .smithers/
  - WORKSPACE.ts
  - FACTORY.ts
  - factory.json
  - home.json
- PACKAGE.ts
- flows/
  - review/
    - flow.mdx
  - deploy/
    - status/
      - flow.ts
  - triage/
    - SKILL.md
- .flows/
  - control.db
  - engine.db
  - logs/
    - run-6f2a9c1b.log
- .gitignore

## Target declarations

`.smithers/WORKSPACE.ts` declares workspace-wide runtime, package-manager, and cache settings. A root `WORKSPACE.ts` is also supported. A `PACKAGE.ts` declares targets for its directory; exported target keys become labels such as `//apps/site:build`.

`.smithers/FACTORY.ts` declares the factory beside the workspace: `export const factory = Smithers.Factory({ summary, flows, on, github })` names the flows the repository features (`Smithers.Flow` declarations under `flows`), the Dispatcher table (`on`, event key to the flow or flows it starts), and the GitHub policy (`Smithers.Github.Policy({ mirror, issues, changes })`); `export const home = Smithers.Factory.Home({ blocks })` is the home pane. The file may import `WORKSPACE.ts` and never imports a `PACKAGE.ts`: a target it needs is named by label, `Smithers.label("//:ci")`. A repository without the file declares no factory.

These are source files to commit. Their presence does not make `.flows/` state part of the source tree. The [CLI reference](/docs/reference/cli/) distinguishes target execution from durable flow commands.

## flows/

One directory per flow; discovery reads these rules.

- The flow id is the directory path below `flows/`: `flows/deploy/status/flow.ts` has the id `deploy/status`.
- A flow directory holds one entry file: `flow.ts` (a module flow), `flow.mdx` (a markdown flow), or `SKILL.md`. When more than one is present, discovery uses the first in that order and warns.
- An entry file directly under `flows/` is refused: path-named sources cannot contain a root-level entry.
- `smthrs init <name>` scaffolds `flows/<name>/flow.mdx`; the name is one path segment of letters, digits, `-`, and `_`.
- Ids that start with `system/` are reserved for the control plane: `smthrs flow plan` and `smthrs flow start` refuse them, and `smthrs flow list` does not list them.
- `.smithers/factory.json` is generated, not authored. A root `PACKAGE.ts` that declares `Smithers.FactoryProjection()` renders it from `.smithers/FACTORY.ts`: `summary`; `flows`, one row per discovered flow with its id, description, path, kind, capabilities, and model, plus the `summary` and `featured` flag the factory declares, featured rows first; `on`, the Dispatcher table flattened to rows of `event`, `flow`, and the `description` the card shows; and `github`, the policy. `smithers-build target //:factoryProjection --write` regenerates it and `ci` fails on drift. See [Featured flows](/docs/reference/flow-mdx/#featured-flows).
- `.smithers/home.json` is generated by the same target. It is the repository's home pane, the first card smithers.sh shows for the repository, declared in `.smithers/FACTORY.ts` as `export const home = Smithers.Factory.Home({ blocks })`. Blocks are declared values, never raw HTML: `Smithers.Home.Text({ text })`, `Smithers.Home.Links({ links })`, `Smithers.Home.Flows()` (the featured flows, read from the `flows` rows of `factory.json`), and `Smithers.Home.CiBenchmark()` (the cold CI time, the one-file-change time, and the cache hit rate, each shown as not measured yet until a measurement exists). A string carrying an HTML tag fails the declaration. A factory that exports no `home` has no home pane, and the projection refuses a stale `home.json` until `--write` removes it.

## .flows/

The default local runtime stores these files under `.flows/`. Workspace cache settings and flow implementations can declare other output locations.

| Path | Written by | Contents |
| --- | --- | --- |
| `.flows/` | `smthrs init`, or the first local command | The project's state directory; its presence alone anchors root resolution. |
| `.flows/control.db` | the control plane | The control database: runs, approvals, and memory facts. |
| `.flows/engine.db` | the durable engine | The engine's execution database, kept separate from the control database. |
| `.flows/logs/` | `smthrs flow start --detached` | One log per detached run. |
| `.flows/logs/<runId>.log` | a detached run | Everything the detached child wrote. The launcher opens it as `pending-<nonce>.log` and renames it onto the run id once the run row is durable. |
| `.flows/logs/<runId>.superseded-<nonce>.log` | `smthrs flow start --detached` | An earlier log moved aside when a relaunched run id took the canonical path. |

On POSIX, opening the Node control store restricts `.flows/` to `0700` and
`control.db`, `control.db-wal`, and `control.db-shm` (when present) to `0600`,
including existing stores. Windows does not use this chmod policy.

Decoded plan inputs and plan card summaries are stored as raw plaintext JSON
for replay. Keep credentials out of plan input: store secrets through
`Credential`, pass a `CredentialRef`, and resolve it only at the adapter that
needs it. File permissions do not encrypt the database or its backups.

## Root resolution

Every command that touches durable state resolves the same project root, in this order:

1. An explicit `--root`, resolved against the invocation directory when relative.
2. Otherwise the nearest ancestor of the working directory that anchors a project: it holds `.flows/`, or it holds `flows/` beside a `package.json`, `.git`, or `.jj`.
3. Otherwise the invocation directory itself. The walk never climbs past a directory holding `.git` or `.jj`.

## Housekeeping

- Inside a git or jj repository, `smthrs init` adds `.flows/` to the repository's `.gitignore` once, under a `# Smithers run state` comment; outside a repository it writes nothing.
- `smthrs gc` deletes terminal runs older than a threshold from both databases; see [Retention](/docs/guides/retention/).

---

# Smithers Cloud CI
https://smithers.sh/docs/reference/cloud-ci/

Smithers Cloud CI is not shipped. Smithers Cloud registers no repository subscriptions, starts no runs from repository events, and publishes no **Smithers CI** check. Everything below this paragraph is the planned design, kept here so that the intended contract is reviewable before it is built. For what runs `PACKAGE.ts` checks today, see [Run CI on Smithers Cloud](/docs/guides/cloud-ci/#what-runs-checks-today). For the shipped cron trigger, see the [Trigger reference](/docs/reference/triggers/).

The declaration surface this design depends on, `S.Automation` and `Package.automations`, exists only as the type-level proof in [`apps/site/examples/cloud-triggers/contract.d.ts`](https://github.com/smithersai/smithers/blob/main/apps/site/examples/cloud-triggers/contract.d.ts). Its README states that it does not implement trigger registration, matching, dispatch, or the `S.Automation` runtime.

## Planned defaults

| Setting | Planned value |
| --- | --- |
| API origin | `https://api.jjhub.tech` |
| CI automation selected during setup | `//:ci` |
| Declaration | An `S.Automation` exported through `Package.automations`. |
| Automatic events | Those declared in `on`; no events added by the name `ci`. |
| Workspace | `.smithers/WORKSPACE.ts` and committed dependency lockfiles. |
| Check name | **Smithers CI** |
| Execution | Isolated Linux workers on Smithers Cloud's Plue infrastructure. |
| Target selection | The complete dependency graph of the referenced target, with normal cache reuse. |
| Review concurrency | A newer source revision cancels earlier unfinished CI for that review. |

## Planned repository activation

Enabling CI would authorize the repository's automation and register its triggers from the default branch or bookmark. Activation would emit no repository event; the next matching event would start work. A target named `ci` without an automation would acquire no triggers.

The selected automation would have to resolve to executable work. Missing references, empty suites, unsupported worker requirements, and invalid declarations would fail CI. An enabled requirement would stay in effect if a revision deleted the automation or changed its triggers to exclude the review.

Routing would use the trusted registered selectors; the work would load from the event's execution revision, and the run would record both revisions. A review could change its checks but not rewrite its own routing policy or grant itself additional credentials.

## Planned execution and caching

A worker would prepare the workspace, resolve declared tools, install dependencies from the lockfiles, and evaluate the work in isolation. Target dependencies would determine order; independent checks could execute concurrently within worker capacity. Cached results would be reused only when declared inputs, execution environment, and cache policy permit. Trigger path filters would decide whether to start work and would not replace target dependency tracking.

Repository credentials would come from the Cloud connection and target or flow declarations. Repository code would not receive the service's infrastructure credentials. Fork execution would require an explicitly authorized isolated policy. A lost worker whose work cannot be recovered would fail the attempt; a rerun could reuse valid completed cache entries. The service deadline would bound queue time, execution, and approval waits.

## Planned results and required checks

The service would publish **Smithers CI** on the tested commit, on the Plue landing request or through the Smithers GitHub App with a link to the run.

| Result | Planned meaning |
| --- | --- |
| Pending | Work is queued, executing, or waiting for an authorized approval. |
| Passed | Required work completed successfully, including valid cached target results. |
| Failed | Configuration, workspace preparation, a check, or unrecoverable execution failed. |
| Cancelled | A user cancelled the attempt or a newer review revision superseded it. |
| Timed out | The run exceeded its deadline. |

Only a passed result would satisfy a required check. For a review revision where CI is required, a selector or predicate that excluded it would produce a failure naming the unmatched requirement rather than an unexplained pending check. Results would stay bound to their attempt and commit; a late result from an earlier revision could not replace the current one. Review runs would test the source commit, not a merge result.

## Status

The shipped surfaces that this design would build on are the `PACKAGE.ts` target graph, `smthrs ci`, the `Smithers.GithubCiGen` target, the cron trigger store and scheduler in `@smthrs/triggers`, and the Smithers GitHub App connection used by the app. When the automation declaration and the event subscriptions land, this page drops the planned framing.

---

# smthrs approve
https://smithers.sh/docs/reference/cli/approve/

:::note[Compatibility command]
For new scripts, use [`smthrs approvals approve`](/docs/reference/cli/approvals/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs approve <approval> [--scope once|run|remembered]
```

## Description

`smthrs approve` records an approval decision for one complete serialized payload: the `approval` field of a `smthrs plan` card, or the `payload` of a parked run's `control.approval.requested` event. Submit the payload unchanged; the digest inside it is what the grant binds to, so an edited payload is a different ask, not an approved one. When the payload answers an in-run ask, the decision restarts the parked run in this same call, and `approve` waits for that run to settle when this process owns the executor, exiting with the run's terminal status. It never plans, never launches a run of its own, and never widens a grant past the `--scope` you pass.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `approval` | JSON string | yes | The complete serialized approval payload, submitted unchanged. |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--scope <scope>` | `once` \| `run` \| `remembered` | `run` | How far the grant reaches: this ask only, the whole run, or every later run. |
| `--remote <url>` | URL | none | Act on a remote control plane. A remote invocation owns no executor, so there is no settlement wait: the printed receipt is the whole answer. |

The interactive default is `run` because an operator affirming a launch matches what `smthrs up` grants; the MCP `resolve_approval` tool defaults to `once` instead, because an argument a client never sent must not widen what it may do.

## Output

Prints the decision receipt. A plan-level decision answers `Accepted` with no `runId`; a decision on an in-run ask answers `Accepted` with the `runId` it restarted. The human rendering indents the receipt; `--json` prints it compact:

```json
{"_tag":"Accepted","receiptId":"<receipt-id>"}
```

## Exit codes

| Code | When |
| --- | --- |
| `0` | The decision was accepted, or the run it restarted settled `completed`. |
| `1` | The run the decision restarted settled `failed`. |
| `2` | The payload was not valid JSON or did not match the approval schema. |
| `3` | The run the decision restarted parked again at `waiting-approval`. |
| `130` | The run the decision restarted settled `cancelled`. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Approve the complete serialized approval payload

USAGE
  smthrs approve [flags] <approval>

ARGUMENTS
  approval string

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory
  --scope choice         How far the grant reaches: this ask only, the whole run (the default, matching `up`), or every later run. The MCP resolve_approval tool defaults to `once` instead, because an argument a client never sent must not widen what it may do (choices: once, run, remembered)

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs deny](/docs/reference/cli/deny/) records the opposite decision.
- [smthrs plan](/docs/reference/cli/plan/) renders the payload this verb approves.
- [smthrs run](/docs/reference/cli/run/) launches the payload once it is approved.
- [Plan, approve, run](/docs/guides/plan-approve-run/) walks the decision flow.
- The [`resolve_approval`](/docs/reference/mcp-tools/) MCP tool is this verb's equivalent, with `decision` set to `"approve"`.

---

# smthrs bug
https://smithers.sh/docs/reference/cli/bug/

## Synopsis

```text
smthrs bug <summary...> [--run <run-id>] [--dry-run] [--yes]
```

## Description

`bug` posts a bug report to `https://bug.smithers.sh/api/bugs` without you pasting project state into a browser. The report carries your summary, the CLI version, the platform and Node version, only the named run and its event digest when `--run` is provided (otherwise no runs). Everything passes through the journal's shared redaction rules before it leaves the machine. It never attaches flow sources or arbitrary files: the only run detail is the digest. To post somewhere else, set `SMITHERS_BUG_ENDPOINT`.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `summary` | string (one or more words) | yes | The one-line summary; the words are joined with spaces. An empty summary exits 2. |

The summary may be quoted or supplied as separate words. Omitting it prompts on a TTY;
on a pipe, the CLI exits 2 before opening project state.

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--run <run-id>` | string | none | Attach this run's event digest: status, cause, turns, calls, refusals, and cost as the run's events record them. |

| `--dry-run` | boolean | false | Preview the exact redacted payload and endpoint without sending. |
| `--yes` | boolean | false | Consent to posting without an interactive confirmation. |

## Output

Before sending, prints the endpoint and exact redacted JSON payload to stderr. Posting requires `--yes` or an interactive TTY confirmation; declining or omitting consent without a TTY exits 2 without posting. `--dry-run` never posts, even with `--yes`, and prints `{ "reported": false, "endpoint": ..., "payload": ... }`.

On success prints `{ "reported": true, "endpoint": ... }`. `--json` prints the same document in compact JSON. The post times out after 15 seconds; an endpoint that cannot be reached, or one that answers a non-OK status, exits 1.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs bug — Submit a redacted bug report to the configured endpoint

Usage: smthrs bug <summary...> [options]

Arguments:
  summary

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --run <string>
  --yes                  Post the previewed report without an interactive confirmation
  --dry-run              Preview the redacted report without posting

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [`smthrs status`](/docs/reference/cli/status/): the diagnosis card computed from the same event digest.
- [`smthrs doctor`](/docs/reference/cli/doctor/): check registry, database, runtime, and provider readiness before reporting.

---

# smthrs cancel
https://smithers.sh/docs/reference/cli/cancel/

:::note[Compatibility command]
For new scripts, use [`smthrs runs cancel`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs cancel <run-id>
```

## Description

`smthrs cancel` makes cancellation durable on the run row and interrupts the run through the control plane. Cancellation reaches everything the run spawned: child flows and in-flight work settle as cancelled with it rather than orphaning. Reach for it to end a run that is still live, including one parked at `waiting-approval`; a run that already settled answers with its own `Terminal` receipt instead of an error, so repeating a cancel is safe. `cancel` never deletes the run row or its journal (`smthrs gc` does that), and it never fails a run: it reports the interruption status, which is why a successful cancel exits 130 rather than 0.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `run-id` | string | yes | The durable run to cancel, as printed by the launch receipt or `smthrs ps`. |

## Output

Prints the run's `Terminal` receipt. A live run this call cancelled reports status `cancelled`; a run that was already terminal reports the status it settled with. The human rendering indents the receipt; `--json` prints it compact:

```json
{"_tag":"Terminal","runId":"<run-id>","status":"cancelled"}
```

## Exit codes

The exit code follows the run's terminal status, because a cancel is an interruption rather than a failure of the command:

| Code | When |
| --- | --- |
| `0` | The run had already settled `completed`. |
| `1` | The run had already settled `failed`. |
| `130` | The run is `cancelled`, whether this call cancelled it or an earlier one did. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Cancel a durable run

USAGE
  smthrs cancel [flags] <run-id>

ARGUMENTS
  run-id string

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs down](/docs/reference/cli/down/) cancels every non-terminal run at once.
- [smthrs ps](/docs/reference/cli/ps/) lists runs and their statuses.
- [smthrs status](/docs/reference/cli/status/) shows what one run is doing.
- [smthrs run](/docs/reference/cli/run/) resumes a parked run instead of ending it.
- [Child flows](/docs/guides/child-flows/) covers what a cancellation reaches.

---

# smthrs claude
https://smithers.sh/docs/reference/cli/claude/

## Synopsis

```text
smthrs claude tick <run-id> [--session <id>] [--after-seq <n>]
smthrs claude node-wait <run-id> <node-id> [--timeout-ms <ms>]
smthrs claude monitor [--session <id>] [--all-runs] [--limit <n>]
smthrs claude subscribe <run-id> [--session <id>]
smthrs claude unsubscribe <run-id> [--session <id>]
```

## Description

`claude` is the wire protocol the Claude Code plugin's `/workflows` mirror speaks: the plugin runs these subcommands and relays the JSON, so it never reads a database and never talks to the control plane itself. Reach for these commands when you debug the mirror or script against it; day to day, the plugin calls them for you. None of them changes run state: the only writes are this session's subscription entries in `.flows/claude-mirror-subscriptions.json`.

The session id comes from `--session`, then the `CLAUDE_CODE_SESSION_ID` environment variable, then the literal `unknown`.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `run-id` (`tick`, `node-wait`, `subscribe`, `unsubscribe`) | string | yes | The run to read or follow. |
| `node-id` (`node-wait`) | string | yes | The node to wait on, as `smthrs output <run-id>` lists it (for example `bash#2`). |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--session <id>` | string | `CLAUDE_CODE_SESSION_ID`, else `unknown` | The Claude session a subscription belongs to. Accepted by every subcommand except `node-wait`. |
| `--after-seq <n>` (`tick`) | integer | `0` | Report nodes whose state moved after this journal sequence. |
| `--timeout-ms <ms>` (`node-wait`) | integer | `30000` | How long to wait for the node before reporting `timedOut`. |
| `--all-runs` (`monitor`) | boolean | `false` | Report transitions for every run, not only the ones this session follows. |
| `--limit <n>` (`monitor`) | integer | `200` | How many recent transitions to print. Must be a positive integer; anything else exits 2. |

## Output

- `tick` prints one mirror frame for the run and re-asserts this session's subscription to it. A frame carries `contract` (2), `runId`, `status`, `seq`, `phases`, `nodes` (each with `nodeId`, `label`, `phase`, `kind`, and `state`), `changed` (the nodes that moved after `--after-seq`), `outputs` (settled values of changed nodes, truncated at 2,000 characters), `approvals` (the parked question and approval payload while the run waits on one), and `timedOut`. A run the project does not know still gets a frame, with `status` set to `unknown`.
- `node-wait` polls every 250 ms until the node settles, the run settles, or the timeout lapses. It prints the settled node with `timedOut: false`; `{ "nodeId": "...", "outcome": "vanished", "status": "...", "timedOut": false }` when the run reaches a terminal status first; or `{ "nodeId": "...", "outcome": "pending", "timedOut": true }` when time runs out. An unknown run id exits 2.
- `monitor` prints notable run transitions as NDJSON, one object per line with `runId`, `at`, `kind`, and, for run-status kinds, `status`. It reports up to `--limit` of the most recent notable events, oldest first: run acceptance, starts, parks, approval requests, terminal statuses, and settled agent calls. Without `--all-runs` it reports only the runs this session follows.
- `subscribe` and `unsubscribe` print `{ "subscriptions": <count> }`, the live entries after the change. An entry expires 24 hours after its last re-assertion.

Every subcommand renders JSON (or NDJSON) as its only output form; the global `--json` flag changes nothing here.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Claude Code plugin mirror protocol

USAGE
  smthrs claude <subcommand> [flags]

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)

SUBCOMMANDS
  tick           Print one mirror frame for a run
  node-wait      Block until one node settles
  monitor        Print notable run transitions as NDJSON
  subscribe      Follow a run in this session's mirror
  unsubscribe    Stop following a run in this session's mirror
```

## See also

- [`smthrs status`](/docs/reference/cli/status/): the diagnosis card for one run.
- [`smthrs logs`](/docs/reference/cli/logs/): the event stream a frame is computed from.
- [Drive Smithers from your agent](/docs/guides/mcp-setup/): the plugin that consumes this protocol.
- [MCP tools](/docs/reference/mcp-tools/): `watch_run` reads the same events over MCP.

---

# smthrs deny
https://smithers.sh/docs/reference/cli/deny/

:::note[Compatibility command]
For new scripts, use [`smthrs approvals deny`](/docs/reference/cli/approvals/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs deny <approval>
```

## Description

`smthrs deny` records a denial for one complete serialized payload: the `approval` field of a `smthrs plan` card, or the `payload` of a parked run's `control.approval.requested` event. Submit the payload unchanged; the digest inside it names the ask being refused. A denied plan stays unapproved, so a later `smthrs run` of the same payload still parks at `waiting-approval`. When the payload answers an in-run ask, the denial restarts the parked run in this same call, and `deny` waits for that run to settle when this process owns the executor, exiting with the run's terminal status. It never plans, never launches a run of its own, and never approves anything.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `approval` | JSON string | yes | The complete serialized approval payload, submitted unchanged. |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--remote <url>` | URL | none | Act on a remote control plane. A remote invocation owns no executor, so there is no settlement wait: the printed receipt is the whole answer. |

## Output

Prints the decision receipt. A plan-level decision answers `Accepted` with no `runId`; a decision on an in-run ask answers `Accepted` with the `runId` it restarted. The human rendering indents the receipt; `--json` prints it compact:

```json
{"_tag":"Accepted","receiptId":"<receipt-id>"}
```

## Exit codes

| Code | When |
| --- | --- |
| `0` | The decision was accepted, or the run it restarted settled `completed`. |
| `1` | The run the decision restarted settled `failed`. |
| `2` | The payload was not valid JSON or did not match the approval schema. |
| `3` | The run the decision restarted parked again at `waiting-approval`. |
| `130` | The run the decision restarted settled `cancelled`. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Deny the complete serialized approval payload

USAGE
  smthrs deny [flags] <approval>

ARGUMENTS
  approval string

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs approve](/docs/reference/cli/approve/) records the opposite decision, with a `--scope` for the grant.
- [smthrs plan](/docs/reference/cli/plan/) renders the payload this verb refuses.
- [smthrs run](/docs/reference/cli/run/) reports the park a denial leaves in place.
- [Plan, approve, run](/docs/guides/plan-approve-run/) walks the decision flow.
- The [`resolve_approval`](/docs/reference/mcp-tools/) MCP tool is this verb's equivalent, with `decision` set to `"deny"`.

---

# smthrs doctor
https://smithers.sh/docs/reference/cli/doctor/

## Synopsis

```text
smthrs doctor
```

## Description

`smthrs doctor` answers "why did that not work?" without launching anything: it inspects the project and prints one report over registry, state, database, runtime, and provider readiness. Each check lands at `ok`, `warn`, or `fail`. A `warn` is a fact you should know that stops nothing; a `fail` is a fact that stops the next command you run. Where a Smithers 0.x database still sits beside the project, it opens the file read-only to count the non-terminal runs it holds, and never writes to it.

## Output

Prints a header naming the project root, then one line per check: its level, its name, and its detail.

```text
smthrs doctor: /work
```

The checks, in order:

| Check | Reports |
| --- | --- |
| `registry` | How many flows discovery found under `flows/`, or why it found none. |
| `registry <path>` | One `warn` per discovery warning, with its code and message. |
| `state` | The `.flows/` state directory, or that it is not created yet. |
| `database <path>` | For `control.db` and `engine.db`: how many migrations are applied, that the file is not created yet, or that Smithers 1.0 did not create it. |
| `node` | The Node version, `fail` below the 22.19.0 floor the durable engine requires. |
| `jj` | The `jj` binary path, or a `warn` when it is missing or not executable. |
| `providers` | Which provider keys are set, and which are exported but empty. |
| `backend` | A `fail` when `SMITHERS_BACKEND` names an unsupported backend. |
| `smithers 0.x` | A `warn` per 0.x marker or database found. A database entry adds how many non-terminal runs it holds, or that it is unreadable. |

The `providers` check reads `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, and `CEREBRAS_API_KEY`, and notes when `SMITHERS_OPENAI_AUTH=chatgpt` supplies the OpenAI credential. The seat resolver also supports routes beyond this readiness list. Credential presence does not verify authentication, quota, or model availability; see [provider troubleshooting](/docs/troubleshooting/doctor-provider/).

`--json` prints the report object verbatim: the root, and every check with its name, level, and detail.

## Exit codes

| Code | Meaning |
| --- | --- |
| `1` | At least one check reported `fail`. The full report prints first; warnings alone exit 0. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs doctor — Check project discovery, providers, tools, and durable-state compatibility

Usage: smthrs doctor [options]

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [smthrs ls](/docs/reference/cli/ls/): list the flows the `registry` check counts.
- [smthrs init](/docs/reference/cli/init/): scaffold the `flows/` directory an empty registry points at.
- [Model seats](/docs/guides/model-seats/): the provider keys and seat strings the `providers` check feeds.
- [Doctor reports a provider problem](/docs/troubleshooting/doctor-provider/): fixes when the `providers` check warns or fails.

---

# smthrs down
https://smithers.sh/docs/reference/cli/down/

:::note[Compatibility command]
For new scripts, use [`smthrs runs cancel-all`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs down
```

## Description

`smthrs down` cancels every run in the project that has not reached a terminal status: `accepted`, `running`, `parked`, and `waiting-approval` runs each get one cancel, while `completed`, `failed`, and `cancelled` runs are left as they are. Reach for it to stop all live work in a project at once, where `smthrs cancel` stops a single run. It never deletes run state; deleting terminal history is what `smthrs gc` does.

## Output

Prints an object with one `cancelled` entry per run it cancelled, pairing the run id with that run's cancel receipt. A project with no non-terminal runs prints an empty `cancelled` list and exits 0. `--json` prints the same document as JSON.

## Exit codes

This verb adds no codes beyond the generic set.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Cancel every non-terminal run

USAGE
  smthrs down [flags]

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs cancel](/docs/reference/cli/cancel/): cancel one run by id.
- [smthrs ps](/docs/reference/cli/ps/): list runs and their statuses.
- [smthrs gc](/docs/reference/cli/gc/): delete terminal runs older than a threshold, with the rows they own.
- [Plan, approve, run](/docs/guides/plan-approve-run/): the run lifecycle `down` cuts short.

---

# smthrs gc
https://smithers.sh/docs/reference/cli/gc/

## Synopsis

```text
smthrs gc [--older-than <duration>] [--dry-run]
```

## Description

`gc` is the retention pass over this project's two state databases, `.flows/control.db` and `.flows/engine.db`. It deletes terminal runs (completed, failed, or cancelled) that finished before the threshold, together with every row those runs own, sweeping both files so no half of a deleted run survives. Reach for it when you bound how much history a project keeps; nothing in the durable stores forgets on its own. It never touches a live run, never deletes a terminal run that a live run stands above or below in the lineage, and never runs against a remote control plane: the pass reads and writes the local `.flows/` files directly.

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--older-than <duration>` | string | `30d` | How long a terminal run must have been finished before the pass deletes it. Spelled as a count and a unit: `s` seconds, `m` minutes, `h` hours, `d` days, `w` weeks, as in `12h`, `90m`, or `2w`. A spelling the command cannot read, including a zero window such as `0s`, exits 2. |
| `--dry-run` | boolean | `false` | Report exactly what a real pass would delete, writing nothing. |

## Output

Prints the sweep document: `olderThan` as typed, `dryRun`, one report per database, and `failures`. Each report names the `database` file, the threshold as `olderThanMs`, the `runs` it deleted oldest first (under `--dry-run`, the runs it would delete), the `deleted` row counts per table (empty under `--dry-run`), and its own `dryRun` marker. A project with no `.flows/` databases reports an empty sweep rather than failing. `--json` prints the same document in compact JSON.

A database the pass cannot open is a failure, never a report of zero runs. Every database is probed before any write, so a failed probe abandons the whole pass: the report still prints, stderr names each file with its reason (`gc could not open 1 database, so nothing was collected from it`), and the command exits 1.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs gc — Collect old terminal runs and compact their journals; separate from target caches

Usage: smthrs gc [options]

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --older-than <string>   (default: 30d)
  --dry-run

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [`smthrs ps`](/docs/reference/cli/ps/): list the runs a sweep considers.
- [`smthrs down`](/docs/reference/cli/down/): cancel runs that are still live.
- [Manage retention](/docs/guides/retention/): the retention workflow end to end.

---

# smthrs init
https://smithers.sh/docs/reference/cli/init/

## Synopsis

```text
smthrs init [name] [--root <directory>]
```

## Description

`smthrs init` creates a `WORKSPACE.ts`, a starter `PACKAGE.ts`, a Markdown flow at `flows/<name>/flow.mdx`, and the `.flows/` state directory. A missing `package.json` is scaffolded with a Node/Pnpm toolchain; an existing supported Yarn, Bun, or Pnpm declaration is reused. Existing declarations and flows are preserved; an existing `.smithers/WORKSPACE.ts` also counts as the workspace declaration. In a git or jj repository it adds `.flows/` to `.gitignore`. Use `--root` to choose the directory and `generate flow <name>` to add another flow later. Seats resolve from provider configuration; the old `init --global` flag remains unsupported.

The scaffolded flow declares a `model:` seat chosen from the first provider credential the environment sets, in the order `smthrs doctor` reports them:

- `ANTHROPIC_API_KEY` resolves `anthropic:claude-sonnet-4-5`.
- `OPENAI_API_KEY` resolves `openai:gpt-5.6-sol`. The ChatGPT session `SMITHERS_OPENAI_AUTH=chatgpt` resolves the same seat.
- `OPENROUTER_API_KEY` resolves `openrouter:anthropic/claude-sonnet-4.5`.
- `MOONSHOT_API_KEY` resolves `moonshot:kimi-k3`.
- `GEMINI_API_KEY`, then `GOOGLE_API_KEY`, resolves `gemini:gemini-2.5-pro`.
- `CEREBRAS_API_KEY` resolves `cerebras:gpt-oss-120b`.

With no credential set, the scaffold still declares the default (`anthropic:claude-sonnet-4-5`), and the frontmatter comment names the key to set. A scaffold without a `model:` line would not be launchable at all.

The starter requests `fs:read:**`, `fs:write:**`, and `proc:spawn:*` for its file-editing and test-running instructions. Narrow these capabilities before running a smaller task; process spawning permits shell commands. Its directory supplies the flow name, so the frontmatter contains no ignored `name:` key.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | The flow name: one path segment of letters, digits, `-`, and `_`. Defaults to a slug of the project directory name, or `flow` when that yields nothing. |

## Output

Prints `root`, the `created` and `retained` declaration paths, the nested `flow` scaffold record, and suggested `next` commands. The flow record includes its file, seat, state directory, and Git ignore result. `--json` prints the machine-readable result.

An invalid name fails with:

```text
a flow name is one path segment of letters, digits, '-' and '_'; got "<name>"
```

## Exit codes

Success exits 0; invalid input or a filesystem failure exits nonzero.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs init — Initialize workspace and target declarations plus a starter flow, preserving existing files

Usage: smthrs init [name] [options]

Arguments:
  name

Options:
  --root <string>  Directory to initialize; defaults to cwd

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [CLI groups](/docs/reference/cli/): use `flow start` to launch the new flow and `flow list` to discover it.
- [Markdown flows](/docs/guides/markdown-flows/): the `flow.mdx` authoring model the scaffold uses.

---

# smthrs logs
https://smithers.sh/docs/reference/cli/logs/

:::note[Compatibility command]
For new scripts, use [`smthrs runs logs`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs logs [run-id] [--follow]
```

## Description

`smthrs logs` reads a run's journaled events. Without `--follow` it prints the recorded history once as a turn-by-turn transcript; with `--follow` it stays attached and prints one line per event as the event lands. Omitting the run id reads the events of every run in the project. Reach for it to watch a live run or to replay a settled one turn by turn. It never prints anything but the journal: the transcript is a projection of events, and `--json` is the raw event stream, byte-stable for scripts.

`smthrs events` is a hidden alias of `smthrs logs --json`.

## Arguments

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `run-id` | string | No | The run to read. Omit it to read every run in the project. |

## Flags

| Flag | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `--follow` | boolean | `false` | Stream events as they are journaled instead of reading the recorded history once. |
| `--json` | boolean | `false` | Print the raw events instead of the transcript projection. |

## Output

Human output is the transcript: a summary header (run id, status, duration, turns, calls, tokens), one `=== turn N ===` header per model turn, and one offset-stamped line per event under it. A run with no events prints `No events.`

`--json` prints the recorded events as one compact JSON array.

In follow mode, human output is one line per event as it lands, because a transcript needs the whole run, and `--json` prints one compact JSON document per event. Follow mode keeps printing until you interrupt it.

An unknown run id fails with `Run not found: "<run-id>"` on stderr and nothing on stdout.

## Exit codes

| Code | When |
| ---- | ---- |
| `0` | The read succeeded, whatever state the run is in. |
| `1` | The read or the follow failed. |
| `2` | The named run does not exist. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Read run events; --follow streams future events

USAGE
  smthrs logs [flags] [<run-id>]

ARGUMENTS
  run-id string     (optional)

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory
  --follow               Keep streaming new run events after the recorded history

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [`smthrs status`](/docs/reference/cli/status/): the diagnosis card computed from the same events.
- [`smthrs output`](/docs/reference/cli/output/): one node's recorded output instead of the whole transcript.
- [The inspecting runs guide](/docs/guides/inspecting-runs/): the read path across `ps`, `status`, and `logs`.
- [The `get_run_events` and `watch_run` MCP tools](/docs/reference/mcp-tools/): the recorded history and a polling follow for an agent.

---

# smthrs ls
https://smithers.sh/docs/reference/cli/ls/

:::note[Compatibility command]
For new scripts, use [`smthrs flow list`](/docs/reference/cli/flow/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs ls
```

## Description

`smthrs ls` prints the flow ids this project offers, one entry per discovered flow with its description. Reach for it to answer "what can I run here" before `smthrs plan` or `smthrs up`. Discovery scans the directories under the project's `flows/` directory for a `flow.mdx`, `flow.ts`, or `SKILL.md` source; [the flow discovery guide](/docs/guides/flow-discovery/) covers what makes a source a flow. The listing never includes the reserved `system/*` flows, and it never reports discovery problems; `smthrs doctor` owns those warnings.

`smthrs workflow list` is a hidden alias that prints the same listing. Every other `workflow` form was removed in 1.0 and refuses with a removal notice.

## Output

Prints the flows document as indented JSON with keys in code-unit order: one item per discovered flow with its `flowId` and `description`. A project with no discovered flows prints an empty `items` array. `--json` prints the same document in compact form.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  List the flows discovered under this project

USAGE
  smthrs ls [flags]

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [`smthrs up`](/docs/reference/cli/up/): plan, approve, and run one listed flow.
- [`smthrs ps`](/docs/reference/cli/ps/): list durable runs instead of flows.
- [The `list_flows` MCP tool](/docs/reference/mcp-tools/): the same listing for an agent.

---

# smthrs mcp
https://smithers.sh/docs/reference/cli/mcp/

`smthrs mcp add` registers the CLI as an MCP server. Incur generates the
server's tools from the same command definitions and Zod schemas used by
`smthrs`, covering targets, durable flow control, and operator commands.

```sh
smthrs mcp add --agent claude-code
smthrs mcp doctor
```

| Option | Purpose |
| --- | --- |
| `--agent <id>` | Select a supported MCP client, such as `claude-code` or `cursor`. |
| `--command <command>`, `-c` | Override the command the client launches. |
| `--no-global` | Install project-local configuration instead of global configuration. |

The client starts `smthrs --mcp` and communicates over stdio until the
connection closes. Long-running commands marked `mcp: false`, such as
`serve`, `watch`, and `triggers serve`, are omitted from the generated tools.
Use `smthrs --llms-full --format json` to inspect the canonical command
schemas. Local-only commands retain their local workspace restrictions when
called through MCP.

```text
smthrs mcp add — Register as MCP server

Usage: smthrs mcp add [options]

Options:
  --agent <string>        Target a specific agent (e.g. claude-code, cursor)
  --command, -c <string>  Override the command agents will run (e.g. "pnpm my-cli --mcp")
  --no-global             Install to project instead of globally
```

## Compatibility and external servers

The exported `@smthrs/cli/McpServer` module retains the previous named-tool
protocol for existing hosts. Its `--surface`, `--allowed-tools`, and
`--read-only` options belong to that compatibility adapter, not the generated
public `smthrs --mcp` command. The historical [MCP tools reference](/docs/reference/mcp-tools/)
describes that older API.

`--mcp-config <path>` on flow commands serves a different purpose: it connects
external MCP servers and exposes their tools to the local flow executor.
It does not register Smithers with a client. See
[`smthrs serve`](/docs/reference/cli/serve/) for the HTTP control-plane host.

---

# smthrs memory
https://smithers.sh/docs/reference/cli/memory/

`memory` reads and writes the same `.flows/control.db` store used by agent
runs. It requires local access and refuses `--remote`.

| Command | Purpose |
| --- | --- |
| `memory list [--prefix <prefix>]` | List facts in a namespace. |
| `memory get <key>` | Read a fact including its value and metadata. |
| `memory set <key> <value>` | Store JSON when the value parses, otherwise store the literal string. |
| `memory rm <key>` | Delete one fact. |
| `memory recall <query>` | Search facts and accepted, current notes. |
| `memory notes list/get/add/status/supersede` | Inspect notes, append new ones, change review status, or supersede older notes. |
| `memory threads list/create/show/rm` | Inspect and manage durable conversation threads. |
| `memory messages list/add` | Page through ordered messages or append by an idempotent message ID. |
| `memory compact <thread>` | Atomically replace selected messages with a supplied summary. |

## Namespaces and facts

The default namespace is `user:cli`. Select another with
`--namespace flow:review`, `agent:<id>`, `user:<id>`, or `global:<id>`.
`--namespace flow --id review` is also accepted. IDs must be nonempty and
cannot contain control characters.

`set` accepts `--ttl-ms` and repeatable `--tag`; `--value-json` requires valid
JSON instead of falling back to a string. Tags use the memory API's declared
prefixes, such as `source:cli` or `scope:project`. List commands accept
`--limit` (default 100).

## Recall and notes

`recall` defaults to `--method keyword`. `--method fts` enables and queries
SQLite FTS for the selected namespace kinds. Repeat `--bank` to query several
banks, and use `--max-tokens` to bound results. Semantic recall remains a
library binding requiring an explicitly configured embedding provider.

`notes add <text>` accepts `--note-id`, `--status`, repeatable `--tag`, and
repeatable `--supersedes`. Notes default to accepted. Use
`notes status <note> pending|accepted|rejected` for the review gate, or
`notes supersede <note> <replacement>` for an existing replacement. Recall
excludes pending, rejected, and superseded notes.

## Conversation history

`threads create` accepts `--thread-id` and `--title`; `threads show <thread>`
returns the thread and ordered messages. `messages add <thread> <text>`
accepts `--message-id`, `--role`, and a Unix-millisecond `--at`. Reusing a
message ID is idempotent. For pagination, supply both `--after-id` and
`--after-at` to `messages list`.

Compaction requires an explicit summary and cutoff:

```sh
smthrs memory compact review-thread --summary "Earlier discussion summary" --before 1700000000000 --keep 10 --dry-run
```

The cutoff is in Unix milliseconds. Remove `--dry-run` to insert the summary
and delete eligible messages in one transaction; the newest `--keep`
messages remain. `threads rm` deletes a thread and its messages.

Use `--root` to select a project and `--json` for machine-readable results.
Get commands return the full stored record, writes report their result, and
missing records fail. Use the command's `--schema` for the exact contract.

---

# smthrs migrate
https://smithers.sh/docs/reference/cli/migrate/

## Synopsis

```text
smthrs migrate [path] [--scan] [--apply] [--seat <seat>] [--allow-unsafe <name,...>] [--acknowledge-run-state] [--allow-no-vcs] [--keep-old-sources] [--unit <id,...>] [--max-repair-rounds <n>] [--report-dir <dir>] [--flows-dir <dir>] [--verify-install <command>] [--verify-format <command>] [--verify-typecheck <command>] [--verify-test <command>]
```

## Description

`smthrs migrate` converts a Smithers 0.x project to the 1.0 authoring model: it rewrites application source onto `Flow`, `Action`, and Effect, archives the old sources, and records every decision in an auditable report. It is the same tool `npx @smthrs/migrate@next` runs, reachable as a verb from this CLI. It never recreates the JSX runtime, and it never rewrites or resumes 0.x run state. The 0.x `--to` flag was removed: storage is SQLite only ([the databases section](/docs/migration/1.0/#databases)).

The verb has three modes. With neither flag, the mode is `plan`; when both are passed, `--scan` wins.

| Mode | What it does |
| --- | --- |
| `scan` | Reads the project and writes nothing. Selected by `--scan`. |
| `plan` | Scans, plans the migration units, and writes only the report. The default. |
| `apply` | Checkpoints, transforms one unit at a time, verifies, archives the old sources, and writes the report. Selected by `--apply`. |

[The 1.0 migration walkthrough](/docs/migration/1.0/) covers the whole path; this page is the verb contract.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | no | The 0.x project to convert. Defaults to the 0.x project root found by walking up from the working directory. |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--scan` | boolean | `false` | Inventory the project and write the report without planning any unit. |
| `--apply` | boolean | `false` | Convert the project source, instead of planning the conversion. |
| `--seat` | string | unset | The model seat the migration's agent runs on. |
| `--allow-unsafe` | string | unset | Accept the named unsafe constructs, or `all`. |
| `--acknowledge-run-state` | boolean | `false` | Accept the 0.x run state the report lists and migrate the source anyway. |
| `--allow-no-vcs` | boolean | `false` | Accept a file copy as the only checkpoint, in a project under no version control. |
| `--keep-old-sources` | boolean | `false` | Leave the 0.x sources in place beside the flows written from them. |
| `--unit` | string | unset | Migrate only these units, comma separated. |
| `--max-repair-rounds` | integer | unset | How many times one unit may be repaired before it is reported as failed. |
| `--report-dir` | string | unset | Where the report is written, relative to the project root. |
| `--flows-dir` | string | unset | Where the written flows go, instead of `flows/`. |
| `--verify-install` | string | unset | The command that installs dependencies, instead of the one the lockfile implies. |
| `--verify-format` | string | unset | The command that formats the project, instead of the one its config implies. |
| `--verify-typecheck` | string | unset | The command that typechecks the project, repeatable; one empty value runs no typecheck at all. |
| `--verify-test` | string | unset | The command that runs the tests, instead of the project's own test script. |

## Output

Prints a summary of the migration: the mode and project root, unit counts by status (planned, migrated, failed, blocked), construct and mapping counts, the run-state verdict, the operator instructions any remaining run state needs, the follow-ups a person must settle, the report path, and the exit status. `--json` prints the full report document instead.

Plan and apply write the report itself to `.smithers-migrate/report.json` and `.smithers-migrate/report.md`; `--report-dir` moves both. Commit `report.md`: it is the record of what the tool changed, what it could not translate, and what a person still has to decide.

A project that still holds non-terminal 0.x runs is refused before the tool runs: the refusal names each such run and tells you to finish, archive, or discard it with the 0.x CLI first, and the command exits 1.

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | The run finished with no failed unit and nothing left blocked in `apply`. |
| `1` | The command failed: a unit failed verification, the layout was refused, the plan was stale, or the project was refused up front for non-terminal 0.x runs or an unreadable 0.x database. |
| `3` | Parked at an operator gate, the project untouched: `apply` found 0.x run state without `--acknowledge-run-state` (`run-state-blocked`), or constructs with no safe translation without `--allow-unsafe <name,...>` or `--allow-unsafe all` (`unsafe-blocked`). An `apply` report that ends with blocked units also exits 3. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs migrate — Inventory, plan, or apply the 0.x-to-1.x source migration

Usage: smthrs migrate [path] [options]

Arguments:
  path

Options:
  --root <string>               Project directory; defaults to the nearest Smithers project
  --remote <string>             Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>         Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>         Path to the configured MCP servers
  --quiet                       Suppress progress messages
  --scan
  --apply
  --seat <string>
  --allow-unsafe <string>
  --acknowledge-run-state
  --allow-no-vcs
  --keep-old-sources
  --unit <string>
  --max-repair-rounds <number>
  --report-dir <string>
  --flows-dir <string>
  --verify-install <string>
  --verify-format <string>
  --verify-typecheck <array>
  --verify-test <string>

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [smthrs doctor](/docs/reference/cli/doctor/): reports the 0.x run state `migrate` refuses to touch.
- [Migration to 1.0](/docs/migration/1.0/): the walkthrough this command is one step of.

---

# smthrs output
https://smithers.sh/docs/reference/cli/output/

:::note[Compatibility command]
For new scripts, use [`smthrs runs output`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs output <run-id> [node-id]
```

## Description

`smthrs output` prints what one step of a run produced, projected from the run's journaled events. Node ids are `<flowName>#<ordinal>`, the ordinal counting that flow's calls within the run from 1, and the reserved id `result` names the run's final assistant output. Reach for it after a run settles, when you want one step's value rather than the whole transcript `smthrs logs` prints. It is read-only: it never reruns, resumes, or edits the run, and a `--remote` read projects the same events as a local one.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `run-id` | string | yes | The run to read. |
| `node-id` | string | no | The node to print. Omit it to print every node the run recorded. |

## Output

With no node id, prints every recorded node in the order the calls started: one entry per flow call the agent made, plus `result` when the run produced a final output. A call that started and never settled is listed with outcome `pending`, which is what a run that died mid-call leaves behind.

With a node id, the human rendering is the node's outcome line, `<node-id> <outcome>`, followed by its recorded message when it has one, then its value: the string itself, or pretty-printed JSON for a structured value.

`--json` prints the node object instead (or, with no node id, the array of node objects): `nodeId`, `flowName`, `outcome` (`success`, `failure`, or `pending`), plus the recorded `input`, `value`, `message`, and event timing and sequence fields when present.

A node id that names nothing prints one of two messages and exits 2:

```text
Run <run-id> has no node <node-id>. It has: <id>, <id>, ...
Run <run-id> recorded no node output.
```

A run id that names no run prints `Run not found: "<run-id>"` and exits 2.

## Exit codes

This verb adds no codes beyond the generic set. An unknown run id or node id is a usage error and exits 2.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Print one registered node output

USAGE
  smthrs output [flags] <run-id> [<node-id>]

ARGUMENTS
  run-id string
  node-id string     (optional)

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs logs](/docs/reference/cli/logs/): the full event transcript of a run.
- [smthrs status](/docs/reference/cli/status/): the diagnosis card for one run.
- [Inspecting runs](/docs/guides/inspecting-runs/): the operator workflow around `ps`, `status`, `logs`, and `output`.
- [MCP tools](/docs/reference/mcp-tools/): the `get_node_detail` tool reads the same node-output projection for an agent.

---

# smthrs plan
https://smithers.sh/docs/reference/cli/plan/

:::note[Compatibility command]
For new scripts, use [`smthrs flow plan`](/docs/reference/cli/flow/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs plan <flow> [key=value...] [--data <json>]
```

## Description

`smthrs plan` resolves `<flow>` from the project registry, compiles its keyed plan, and prints the plan card: the node graph with each node's cache status, the plan digest, and the complete `approval` payload. Reach for it when you want to review exactly what a launch would do before anything runs; the card is what `smthrs approve`, `smthrs deny`, and `smthrs run` all consume. It never starts a run, never approves anything, and never writes flow events: the payload it prints is inert until you submit it.

Flow ids that start with `system/` are reserved for the control plane and carry no body in 1.0. Naming one exits 1:

```text
smthrs plan system/replay: system/replay is a reserved system flow id and carries no body in 1.0.0-rc.0, so a launch would park with nothing to run. Name a flow from `smthrs ls`. See https://smithers.sh/migration/1.0#flows
```

Omitting the flow with terminal stdin opens the discovered flow picker. With piped stdin, the command exits 2 and names the missing argument and `--wizard`.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `flow` | string | yes | Flow id from the project registry, as listed by `smthrs ls`. |
| `key=value` | string | no | One input field, repeatable. A pair with no `=` sets the key to `true`. |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--data <json>` | JSON string | none | Flow input as one JSON document. A JSON object merges over any `key=value` pairs; any other JSON value is passed as the `data` field. |

## Output

Prints the plan card as one JSON document. The human rendering indents it; `--json` prints it compact for pipelines:

| Field | Contents |
| --- | --- |
| `planId` | The persisted plan's id. |
| `flowId` | The flow that was planned. |
| `digest` | The digest every later approval binds to. |
| `inputSummary` | The canonical form of the decoded input. |
| `envelope` | The capability, flow, and budget envelope the launch runs under. |
| `deployClass` | The flow's deploy-class flag. |
| `nodes` | One entry per keyed node, with status `cached` (step cache hit) or `run`. |
| `approval` | The complete serialized approval payload: `target`, `scope`, and `idempotencyKey`. |

The `approval` field is the exact argument the decision verbs take:

```bash
approval="$(smthrs --json plan deploy/status | jq -c '.approval')"
```

## Exit codes

| Code | When |
| --- | --- |
| `0` | The plan card rendered. |
| `1` | The flow id is reserved (`system/*`). |
| `2` | The invocation was wrong, for example malformed `--data` JSON. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Render a flow plan and its complete approval payload

USAGE
  smthrs plan [flags] <flow-id> [<key=value...>]

ARGUMENTS
  flow-id string
  key=value... string     (optional)

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory
  --data string          Flow input as JSON; object members override key=value entries

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs run](/docs/reference/cli/run/) launches the payload this verb prints.
- [smthrs up](/docs/reference/cli/up/) plans, approves, and launches in one command.
- [smthrs approve](/docs/reference/cli/approve/) records the decision the payload asks for.
- [smthrs deny](/docs/reference/cli/deny/) records the opposite decision.
- [Plan, approve, run](/docs/guides/plan-approve-run/) walks the three-step flow.
- The MCP server has no plan-only tool; [`run_flow`](/docs/reference/mcp-tools/) plans, approves, and launches in one call.

---

# smthrs ps
https://smithers.sh/docs/reference/cli/ps/

:::note[Compatibility command]
For new scripts, use [`smthrs runs list`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs ps [--flow <id>] [--status <status>]
```

## Description

`smthrs ps` prints the durable runs of this project, optionally narrowed to one flow or one status. Reach for it to see what is running, what is parked, and what settled, and to find the run id a `smthrs status` or `smthrs logs` read needs. A run that has sat at `accepted` with no owner process past the launch handoff is labelled as waiting on an `executor`, which distinguishes it from a run a live process owns. It never reads run events; `smthrs status` and `smthrs logs` do that.

## Flags

| Flag | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `--flow` | string | All flows | Only list runs of this flow id. |
| `--status` | choice | All statuses | Only list runs in this state: `accepted`, `running`, `parked`, `waiting-approval`, `cancelled`, `completed`, or `failed`. An unrecognized value is a usage error (exit 2), not an empty list. |

## Output

Prints the runs listing as indented JSON with keys in code-unit order: one item per durable run with its run id, flow id, status, waiting reason, and timestamps. `--json` prints the same document in compact form.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  List durable runs

USAGE
  smthrs ps [flags]

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory
  --flow string          Only list runs of this flow id
  --status choice        Only list runs with this lifecycle status (choices: accepted, running, parked, waiting-approval, cancelled, completed, failed)

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [`smthrs status`](/docs/reference/cli/status/): the diagnosis card for one listed run.
- [`smthrs logs`](/docs/reference/cli/logs/): the event transcript behind a listed run.
- [The inspecting runs guide](/docs/guides/inspecting-runs/): the read path across `ps`, `status`, and `logs`.
- [The `list_runs` MCP tool](/docs/reference/mcp-tools/): the same listing for an agent.

---

# smthrs run
https://smithers.sh/docs/reference/cli/run/

`smthrs run <pattern>` executes run-kind `PACKAGE.ts` targets, such as agent
tasks, development servers, generators, or explicitly declared publishing
work. Run-kind targets are excluded from `smthrs ci`.

```sh
smthrs run //apps/site:dev --plan
smthrs run //apps/site:dev
```

Use a target declared by your workspace. Patterns include `//...`,
`//package/...`, and `//package:target`. `smthrs target <label>` or a bare
`smthrs //package:target` executes one exact declaration using its own kind.

| Option | Purpose |
| --- | --- |
| `--workspace <path>` | Select the workspace. |
| `--plan` | Print the graph and keys without executing it. |
| `--jobs <count>` | Bound concurrent target execution. |
| `--no-cache` | Bypass result reuse. |
| `--json` | Print the Incur machine-readable result. |

See `smthrs run --help` and `--schema` for the installed argument contract.
`smthrs explain <label>` shows the planned key and local cache state.

## Durable flows and compatibility

Start a durable workflow with `smthrs flow start <flow>`, execute a separately
approved plan with `smthrs flow execute <payload>`, and resume a parked run
with `smthrs runs resume <run-id>`.

The older `smthrs run '<JSON approval payload>'` and
`smthrs run --resume <run-id>` forms remain compatibility paths. They use the
old control handler and retain its receipt/output contract; they do not
select targets. New scripts should use the explicit `flow` and `runs` groups.

---

# smthrs serve
https://smithers.sh/docs/reference/cli/serve/

## Synopsis

```text
smthrs serve [--host <host>] [--port <port>] [--listen]
```

## Description

`smthrs serve` hosts this project's control plane over HTTP and WebSocket, and keeps serving until the process is interrupted. Reach for it when a client that is not this process needs the control plane: another terminal driving the project with `--remote`. A loopback bind (`127.0.0.1`, `::1`, or `localhost`) needs no bearer, but admits only loopback Host values and browser origins; Origin-less CLI requests remain accepted. It never binds any other address without both `--listen` and a bearer token, because an unauthenticated control plane on a network address can launch agents with your credentials.

The server mounts these routes:

| Path | Protocol | Serves |
| --- | --- | --- |
| `/rpc` | http | control rpc |
| `/rpc/ws` | ws | control rpc, including watch |
| `/projections` | http | projection snapshots |
| `/projections/ws` | ws | projection subscriptions |
| `/sync` | http | journal sync |
| `/sync/ws` | ws | journal sync stream |
| `/health` | http | workspace identity |

`GET /health` is unauthenticated. It answers the workspace's identity: a hash of the project root, the gateway id, the protocol version, and the CLI version. A supervisor probes it to learn which workspace a gateway belongs to.

The canonical `serve` command also hosts the durable trigger scheduler over the same Control service. Registered cron schedules and occurrences queued with `triggers fire` are dispatched while it runs, preserving approval requirements. Use `triggers serve` for a standalone scheduler. The hidden `gateway` spelling retains the legacy server path; its 0.x `status` and `stop` subcommands remain unsupported.

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--host` | string | `127.0.0.1` | The address to bind. |
| `--port` | integer | `3000` | The port to bind. |
| `--listen` | boolean | `false` | Allow a non-loopback bind. A bearer token is still required. |
| `--credential` | string | unset | Compatibility flag; warns on stderr. Prefer `SMITHERS_API_KEY` to avoid argv exposure. A bearer credential is required for a non-loopback bind. |
| `--quiet` | boolean | `false` | Do not print the listening banner. |

## Output

Once the server is listening, prints a banner to stderr naming the base URL, every mounted route with its URL and purpose, and the auth mode (`no bearer (loopback Host; loopback browser Origin)` or `bearer token`):

```text
smthrs serve listening on http://127.0.0.1:3000
```

To suppress the banner, pass `--quiet`. `--json` changes nothing: `serve` prints no document. After the banner the server answers requests and prints nothing more.

A non-loopback bind without `--listen` exits 1:

```text
Refusing to bind <host>: pass --listen to serve on a non-loopback address.
```

A non-loopback bind with no bearer token exits 1:

```text
Refusing to bind <host> without a bearer token: set SMITHERS_API_KEY (preferred) or pass --credential.
```

## Exit codes

This verb adds no codes beyond the generic set; both bind refusals exit 1.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs serve — Host the control gateway and durable trigger scheduler

Usage: smthrs serve [options]

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages
  --host <string>         (default: 127.0.0.1)
  --port <number>         (default: 3000)
  --listen

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [smthrs ps](/docs/reference/cli/ps/): pass `--remote` to act on a served control plane from another terminal.
- [Control plane](/docs/guides/control-plane/): run the control plane for a team.
- [HTTP API](/docs/reference/http-api/): the routes `serve` mounts.
- [MCP tools](/docs/reference/mcp-tools/): `list_pending_approvals` and `resolve_approval` answer approvals against the same control plane, over the agent's stdio connection.

---

# smthrs signal
https://smithers.sh/docs/reference/cli/signal/

:::note[Compatibility command]
For new scripts, use [`smthrs runs signal`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs signal <run-id> <signal-json>
```

## Description

`smthrs signal` delivers one named JSON signal to a durable run and prints the delivery receipt. Reach for it when a run is parked on a named wait and an external system holds the answer it waits for; to send an agent a text message instead, use `smthrs steer`. The delivery key combines the run id with a digest of the canonical payload, so re-sending the identical signal replays the original receipt rather than delivering twice. It never queues a signal the run cannot take: a settled run answers with a Terminal receipt, and a run parked on a differently named wait fails with a `no_matching_wait` error that points at `smthrs status`.

## Arguments

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `run-id` | string | Yes | The durable run to deliver to. |
| `signal-json` | JSON string | Yes | The signal as one JSON argument: an object with a string `name` and any JSON `payload`, for example `{"name":"shipped","payload":{"approved":true}}`. |

## Output

On success, prints the control receipt as an indented JSON document with keys in code-unit order: `Accepted` with the receipt id and run id on first delivery, `AlreadyApplied` when the identical payload was already delivered, or `Terminal` with the run's status when the run had already settled. `--json` prints the same receipt in compact form.

A failure prints one line on stderr and nothing on stdout.

## Exit codes

| Code | When |
| ---- | ---- |
| `0` | The receipt is `Accepted` or `AlreadyApplied`, or `Terminal` for a completed run. |
| `1` | Delivery failed: the run does not exist, the run is parked on a differently named wait, or the receipt is `Terminal` for a failed run. |
| `2` | `<signal-json>` is not valid JSON (`signal-json must be valid JSON`) or does not match the payload schema (`signal-json must match the expected payload schema:`). |
| `130` | The receipt is `Terminal` with status `cancelled`. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Deliver a durable JSON signal to a run

USAGE
  smthrs signal [flags] <run-id> <signal-json>

ARGUMENTS
  run-id string
  signal-json string

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [`smthrs steer`](/docs/reference/cli/steer/): send an attributed text message instead of a named signal.
- [`smthrs status`](/docs/reference/cli/status/): see what a run is waiting for before you signal it.
- [The durable waits guide](/docs/guides/durable-waits/): how a flow parks on a named wait and what a signal completes.

---

# smthrs status
https://smithers.sh/docs/reference/cli/status/

:::note[Compatibility command]
For new scripts, use [`smthrs runs show`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs status [run-id]
```

## Description

`smthrs status` answers "what is this run doing, and what does it need". With a run id it prints the diagnosis card computed from that run's own journaled events; without one it prints the same run listing as `smthrs ps`. Reach for it when a run is stuck, parked, or failed and you need the cause plus the exact next command. It never changes the run: the card is a read-only projection, and against `--remote` it renders exactly what a local read renders.

`smthrs inspect` and `smthrs why` spell the same command; both aliases are hidden from `--help`.

## Arguments

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `run-id` | string | No | The run to diagnose. Omit it to list every durable run. |

## Flags

| Flag | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `--json` | boolean | `false` | Print the stable runs-listing document, filtered to the named run, instead of the diagnosis card. |

## Output

With a run id, prints the card as plain text: a `Verdict` line (the status plus the reason that best explains it, such as the journaled failure cause or the question a parked run is asking), a `Run` line (run id, flow id, seat, duration), an `Activity` line (turns, calls, refusals, duplicates, edits), a `Tokens` line, and up to three `Refusals` lines. When they apply, a `Cause` line, an `Output` line, and an `Unblock` line follow; a parked run's `Unblock` is the ready-to-paste `smthrs approve` and `smthrs run --resume` pair. Every card ends with a `Next` line pointing at `smthrs logs`.

With `--json`, or with no run id, prints the runs listing in the `smthrs ps` shape: indented or compact JSON, one item per run.

An unknown run id fails with `Run not found: "<run-id>"` on stderr and nothing on stdout.

## Exit codes

| Code | When |
| ---- | ---- |
| `0` | The read succeeded, whatever state the run is in. The exit code reports the command, never the run's status. |
| `1` | The read itself failed. |
| `2` | The named run does not exist. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Show the diagnosis card for one run, or the run listing

USAGE
  smthrs status [flags] [<run-id>]

ARGUMENTS
  run-id string     (optional)

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [`smthrs ps`](/docs/reference/cli/ps/): the run listing `status` prints without a run id.
- [`smthrs logs`](/docs/reference/cli/logs/): the event transcript the card's `Next` line points at.
- [`smthrs output`](/docs/reference/cli/output/): one node's recorded output from the same journal.
- [The inspecting runs guide](/docs/guides/inspecting-runs/): the read path across `ps`, `status`, and `logs`.
- [The `explain_run` and `get_run` MCP tools](/docs/reference/mcp-tools/): the card and the run summary for an agent.

---

# smthrs steer
https://smithers.sh/docs/reference/cli/steer/

:::note[Compatibility command]
For new scripts, use [`smthrs runs steer`](/docs/reference/cli/runs/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs steer <run-id> --message <text>
```

## Description

`smthrs steer` records one text message against a run and prints the receipt. The message is durable, attributed to the operator principal `cli` with a timestamp, and delivered to the run at its next turn boundary. Reach for it to redirect an agent mid-run: correct its course, add context, or answer a question the run did not park on. It never takes a run over: `steer --takeover` was removed in 1.0 and now exits 1 pointing at [the supervision section of the 1.0 migration guide](/docs/migration/1.0/#supervision).

## Arguments

| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `run-id` | string | Yes | The durable run to steer. |

## Flags

| Flag | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `--message` | string | None (required) | The message text delivered to the run. Omitting it fails as a usage error before anything is recorded. |

## Output

On success, prints the control receipt as an indented JSON document with keys in code-unit order: `Accepted` with the message id as the receipt id and the run id, or `Terminal` with the run's status when the run had already settled and nothing was delivered. Each invocation mints a fresh message id (`cli:steer:<run-id>:<uuid>`), so two invocations deliver two messages rather than one replayed receipt. `--json` prints the same receipt in compact form.

A failure prints one line on stderr and nothing on stdout.

## Exit codes

| Code | When |
| ---- | ---- |
| `0` | The receipt is `Accepted`, or `Terminal` for a completed run. |
| `1` | The command failed: the run does not exist, the receipt is `Terminal` for a failed run, or a removed flag such as `--takeover` was passed. |
| `2` | The invocation was wrong, for example a missing `--message`. |
| `130` | The receipt is `Terminal` with status `cancelled`. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Send a durable, attributed steering message to a run

USAGE
  smthrs steer [flags] <run-id>

ARGUMENTS
  run-id string

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory
  --message string       Text to deliver as an attributed steering message to the run

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [`smthrs signal`](/docs/reference/cli/signal/): deliver a named JSON signal that completes a wait.
- [`smthrs status`](/docs/reference/cli/status/): read a run before you steer it.
- [The steering guide](/docs/guides/steering/): how steering messages reach a running agent.

---

# smthrs suggest
https://smithers.sh/docs/reference/cli/suggest/

## Synopsis

```text
smthrs suggest [<path>] [--seat <provider:model>] [--list] [--json]
```

## Description

`smthrs suggest` is the first command to run in a repository. It picks a model seat from what is already set up on the machine, reads the project, and checks an ordered list of ways Smithers can help. Each suggestion is printed the moment it matches, while the scan keeps going. When the scan ends, the command asks which suggestion to implement, implements it with the same seat, lists the files it wrote, and offers follow-ups for that suggestion.

The scan checks cheap, high-value suggestions first: a test target that reruns only what changed, a lint target, a review flow for pull requests, an `AGENTS.md` generated from the packages, a release-notes flow, a flow for a repeated task the repository's scripts reveal. A suggestion that needs a lot of new code or a large change to the repository is held back and offered as a follow-up after a smaller one has landed.

The command writes files and never commits: the implementing step runs over a kernel-guarded filesystem pinned to the project root, with `.git/` and `.flows/` denied and no permission to run a process at all.

Under `--list` it prints the suggestions and exits without asking; under `--json` it prints one JSON document per suggestion as it is found and never prompts. A session whose input and output are not both terminals (a pipe, a log file, `CI=true`) behaves like `--list` without being asked to, because a question nobody can answer is a hang.

## Seat selection

The seat is the first available entry in this order. `suggest` never uses an Anthropic seat, because that provider does not support this use.

| Order | Seat | Available when |
| --- | --- | --- |
| 1 | Codex subscription, through the default harness | `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`) holds a usable ChatGPT session; `SMITHERS_OPENAI_AUTH=chatgpt` explicitly selects this mode but still requires credentials |
| 2 | Kimi K3 | `MOONSHOT_API_KEY` is set |
| 3 | Any other known provider key | the first set of `OPENAI_API_KEY`, `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `OPENROUTER_API_KEY`, `CEREBRAS_API_KEY` |

`--seat provider:model` overrides the order. When nothing is available, the command prints each seat it looked for and how to set it up, and exits 1.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | no | The project to read. Defaults to the project root found from the working directory. |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--seat <provider:model>` | string | none | The model seat to use for the scan and the implementation, instead of the first available one. |
| `--list` | boolean | `false` | Print the suggestions and exit without asking which to implement. |
| `--json` | boolean | `false` | Print one JSON document per suggestion as it is found, then one for the seat and one for the outcome. Never prompts. |

## Output

The human rendering opens with the seat, prints one line per suggestion as the scan finds it, and closes with what it wrote and the command that runs it. Nothing is committed.

`--json` writes one document per line, in this order.

| Document | When | Fields |
| --- | --- | --- |
| `suggestion` | The moment its check matches | `position`, `id`, `title`, `why`, `effort`, `followUp`, `followUps`, `files` |
| `seat` | Once, after the scan | `seat`, `source` (the candidate id, or `override`), `label` |
| `outcome` | Last | `status`, `root`, `seat`, `suggestions`, `implemented` |

`status` is `listed` for a run that only printed, `implemented` when a suggestion was written, `cancelled` for a cancelled prompt, and `nothing` when every match was held back as too large to offer first. Each entry of `implemented` names the `kind` (`suggestion`, `follow-up`, or `held-back`), the `suggestion` it belongs to, the `files` written, the `command` that runs them, and the agent's `notes`.

## Exit codes

| Code | When |
| --- | --- |
| `0` | A suggestion was implemented, or `--list` and `--json` finished. |
| `1` | No seat is available, the scan failed, or the implementation failed. |
| `2` | The invocation was wrong: a malformed `--seat`, an `anthropic:` seat, or a `path` that is not a directory. |
| `130` | The prompt was cancelled. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs suggest — Discover ways Smithers can help; interactively choose one to implement

Usage: smthrs suggest [path] [options]

Arguments:
  path

Options:
  --root <string>
  --seat <string>
  --list

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [CLI quickstart](/docs/cli-quickstart/) scaffolds and runs a flow, then introduces suggestions.
- [smthrs init](/docs/reference/cli/init/) scaffolds a flow by hand instead.
- [Model seats](/docs/guides/model-seats/) explains `provider:model` and the credentials each provider reads.

---

# smthrs up
https://smithers.sh/docs/reference/cli/up/

:::note[Compatibility command]
For new scripts, use [`smthrs flow start`](/docs/reference/cli/flow/). This page documents the retained flat command and its output format.
:::

## Synopsis

```text
smthrs up <flow> [--data <json>] [-d]
```

## Description

`smthrs up` is the one-command path around the plan, approve, run sequence: it plans `<flow>`, approves the plan with scope `run`, and launches it in one invocation. Reach for it when a flow needs no human review of its plan; reach for `smthrs plan` when it does. Attached, `up` behaves like `smthrs run`: it stays until the run settles and exits with the run's terminal status. With `-d`, it spawns a child process that outlives the launcher and returns as soon as the child proves the run row is durable. `up` never resumes an existing run and never prompts: the approval it grants covers this launch and its whole run.

Flow ids that start with `system/` are reserved and refused with exit 1, exactly as `smthrs plan` refuses them. The 0.x flags are removed rather than ignored: `--serve`, `--interactive`, `--supervise`, `--herdr`, `--monitor`, `--report`, `--force`, `--steal-ownership`, `--resume-claim-owner`, `--resume-claim-heartbeat`, `--resume-restore-owner`, `--resume-restore-heartbeat`, and `--max-concurrency` each exit 1 with a migration link.

Omitting the flow with terminal stdin opens the discovered flow picker. With piped stdin, the command exits 2 and names the missing argument and `--wizard`.

## Arguments

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `flow` | string | yes | Flow id from the project registry, as listed by `smthrs ls`. |

## Flags

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--data <json>` | JSON string | none | Flow input as one JSON document. A JSON object becomes the input fields; any other JSON value is passed as the `data` field. |
| `-d`, `--detached` | boolean | `false` | Launch in a detached child process and return after admission instead of waiting for the run to settle. |
| `--remote <url>` | URL | none | Act on a remote control plane. `-d` is refused here, because a detached launch spawns a local executor: `up -d` against `--remote` exits 1. |

## Output

Attached, `up` prints the same launch receipt as [`smthrs run`](/docs/reference/cli/run/): an `Accepted` document whose `runId` names the run, or a `Parked` document when the run stops at `waiting-approval` on an in-run ask.

Detached, the child announces admission by writing one line to its own log the moment the control plane accepts the run:

```text
SMITHERS_DETACHED_ADMISSION=run:<nonce> runId=<run-id>
```

The launcher waits for exactly that line, renames the log to `.flows/logs/<runId>.log`, and prints the launch document:

```json
{"detached":true,"logFile":"<project>/.flows/logs/<runId>.log","runId":"<runId>"}
```

`runId` is the receipt's own field; there is no `--run-id`, so this document is the only place a caller learns which run started. A child that exits before writing the line fails the launch with exit 1 and the log's tail attached. `SMITHERS_DETACHED_ADMISSION_TIMEOUT_MS` sets how long the launcher waits for admission (default 30000 ms); a child still alive at the deadline is reported as booting on stderr and granted up to four times that window before it is terminated and the launch fails. See [Environment variables](/docs/reference/environment-variables/).

## Exit codes

| Code | When |
| --- | --- |
| `0` | Attached: the launch was accepted or the run settled `completed`. Detached: the child proved admission. |
| `1` | The run settled `failed`, the executor declined the launch, a detached child never reached admission, or the invocation was refused (a reserved `system/*` flow id, a removed 0.x flag, or `-d` against `--remote`). |
| `2` | The invocation was wrong, for example malformed `--data` JSON. |
| `3` | The attached run parked at `waiting-approval` on an in-run ask. Answer with `smthrs approve`. |
| `130` | The attached run settled `cancelled`. |

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
DESCRIPTION
  Plan, approve, and run one flow; -d launches it detached

USAGE
  smthrs up [flags] <flow>

ARGUMENTS
  flow string

FLAGS
  --credential string    Bearer token for the remote control plane; prefer SMITHERS_API_KEY to avoid exposing argv
  --json                 Print the machine-readable document instead of the human rendering
  --remote string        http(s) URL of the control plane to act on; falls back to SMITHERS_REMOTE
  --quiet                Suppress banners and progress on stderr; stdout documents still print
  --mcp-config string    Path to the JSON array of MCP servers the local executor projects into a run's flow catalog
  --root string          Project root to act on, instead of walking up from the working directory
  --data string          Flow input as JSON; object members override key=value entries
  --detached, -d         Launch a local executor in the background and print its run id and log path

GLOBAL FLAGS
  --help, -h                                                          Show help information
  --version, -v                                                       Show version information
  --wizard                                                            Start wizard mode for a command
  --completions <bash|zsh|fish|sh>                                    Print shell completion script (choices: bash, zsh, fish, sh)
  --log-level <all|trace|debug|info|warn|warning|error|fatal|none>    Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none)
```

## See also

- [smthrs plan](/docs/reference/cli/plan/) renders the plan without launching.
- [smthrs run](/docs/reference/cli/run/) launches an approved payload or resumes a parked run.
- [smthrs cancel](/docs/reference/cli/cancel/) ends the run durably.
- [Plan, approve, run](/docs/guides/plan-approve-run/) shows the three-step path this verb compresses.
- The [`run_flow`](/docs/reference/mcp-tools/) MCP tool is this verb's equivalent: it plans, approves for the run, and launches in one call.

---

# smthrs update
https://smithers.sh/docs/reference/cli/update/

## Synopsis

```text
smthrs update
```

## Description

`update` reads the npm registry's dist-tags for `@smthrs/cli` (at `https://registry.npmjs.org/-/package/@smthrs/cli/dist-tags`) and compares them with the installed version. A release candidate publishes under the `next` tag, so the check compares `next` first and `latest` second, which keeps an rc install from being told to downgrade to an older stable. Reach for it before you upgrade the CLI. It prints the install command and never installs anything itself.

## Output

When the installed build is current, the command prints one line:

```text
@smthrs/cli 1.0.0-rc.1 is current.
```

When a later tag exists, it prints which version is available under which tag, then the install command:

```text
@smthrs/cli 1.0.0-rc.1 is available (next); you have 1.0.0-rc.0.
npm install -g @smthrs/cli@1.0.0-rc.1
```

`--json` prints the same text as a quoted JSON string. A registry the command cannot reach exits 1.

## Help output

The flag list above is curated. This is what the binary prints, captured by `apps/site/scripts/gen-cli-data.mjs`.

```text
smthrs update — Check the registry for newer CLI versions; does not install them

Usage: smthrs update [options]

Options:
  --root <string>        Project directory; defaults to the nearest Smithers project
  --remote <string>      Remote control plane URL; defaults to SMITHERS_REMOTE
  --credential <string>  Remote credential; defaults to SMITHERS_API_KEY
  --mcp-config <string>  Path to the configured MCP servers
  --quiet                Suppress progress messages

Custom Global Options:
  --audience <auto|human|agent>  Presentation audience; auto detects agent harnesses, CI, and terminals (default: auto)
  --silent                       Suppress progress; retain the result and actionable failures
  --ui <auto|tty|stream|plain>   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 (default: auto)

Global Options:
  --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
  --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
```

## See also

- [`smthrs doctor`](/docs/reference/cli/doctor/): report registry, database, runtime, and provider readiness.
- [`smthrs bug`](/docs/reference/cli/bug/): report a problem with a run digest attached.

---

# Subpackages
https://smithers.sh/docs/reference/subpackages/

Smithers is not one package. It is a durable execution engine, an agent loop,
a control plane, and a command line, and most of those are published separately
so you can take the part you need without the rest.

Installing [`@smthrs/cli`](https://cli.smithers.sh) gets you the whole product.
Everything below is what it is built from, and most of it is usable on its own:
you can run the durable engine with no agent, or embed the agent loop in your
own service with no command line. The few packages marked workspace-private
below are documented for contributors and are not installable from npm: their
examples run inside this repository, against the workspace copy.

Every package has its own documentation site, linked from its heading. The
aggregate API reference for all of them lives under
[Packages](/docs/reference/api/).

## Command line and control plane

What you reach for to start a run, approve what it asks for, and watch it work.

### [@smthrs/cli](https://cli.smithers.sh)

Command line for starting durable agent runs, approving what they ask for, and reading back what they did.

```bash
npm install --global @smthrs/cli@next
smthrs doctor
smthrs init hello
smthrs flow start hello
smthrs runs list
```

### [@smthrs/control](https://control.smithers.sh)

Plans, approves, starts, watches, steers, and cancels long-running agent runs from any process, recording every decision beside the state it changed.

```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"
  })
})
```

### [@smthrs/gateway](https://gateway.smithers.sh)

Puts a workspace of running agents on an HTTP port so dashboards, scripts, and other machines can watch and steer them.

```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))
```

### [@smthrs/mcp](https://mcp.smithers.sh)

Connects to a Model Context Protocol server and turns each tool it offers into a flow a Smithers agent can call.

```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", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] })
  return yield* source.bindings()
}))
```

### [@smthrs/migrate](https://migrate.smithers.sh)

Rewrites an old Smithers 0.x JSX project onto the 1.0 Flow authoring model, one unit at a time, with an auditable report.

```bash
npx @smthrs/migrate@next --scan
npx @smthrs/migrate@next
npx @smthrs/migrate@next --apply --seat anthropic:claude-sonnet-4-5
```

### [@smthrs/notifications](https://notifications.smithers.sh)

Durable queue for handing a message to a running agent, delivered only at a point in the run where reading it is safe.

```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))
```

### [@smthrs/create-app](https://create-app.smithers.sh)

Turns a set of Smithers flows into a deployable web app whose pages, panes, and flows are named by file location.

```ts
import { CreateApp } from "@smthrs/create-app"

export const App = CreateApp({
  name: "ledger",
  brand: { name: "Ledger", tokens: { accent: "#5288c2" } },
  deploy: { cloudflare: { workerName: "ledger", domain: "ledger.example.com" } }
})
```

## The agent

The agent loop itself, and the pieces it calls out to while it works.

### [@smthrs/agent](https://agent.smithers.sh)

Runs a coding agent as a durable program, so every model call and tool call is journaled, replayable after a crash, and typed.

```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}.`
})
```

### [@smthrs/harness](https://harness.smithers.sh)

Runs an agent loop where the model writes JavaScript instead of calling tools, with every outside call durable and permission gated.

```ts
import * as Cell from "@smthrs/harness/Cell"
import * as QuickJSSandbox from "@smthrs/harness/QuickJSSandbox"

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 })
})))
```

### [@smthrs/chain](https://chain.smithers.sh)

Runs a model-driven agent loop that survives a crash, journaling every tool call so a resumed run repeats no side effects.

`@smthrs/chain` is **workspace-private**: the example below runs inside this repository, not from an npm install.

```ts
import { Author, Catalog, Chain, Journal, QuickJsRunner } from "@smthrs/chain"
import { Effect, Layer } from "effect"

const script = "```flow\nreturn done({ ok: true })\n```"
const layers = Layer.mergeAll(Journal.layerMemory(), Author.layerMock([script]), QuickJsRunner.layer(), Catalog.layer(Catalog.system))

const outcome = await Effect.runPromise(Chain.run({ goal: "check the build" }).pipe(Effect.provide(layers)))
```

### [@smthrs/model](https://model.smithers.sh)

Calls Anthropic, OpenAI, and OpenAI-compatible models through one typed streaming interface, so provider differences stay out of your code.

```ts
import { Model, ModelEvent, ModelRequest, RequestExecutor, Route } from "@smthrs/model"
import { Effect, Layer, Redacted, Result, Stream } from "effect"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
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.")], tools: [], params: ModelRequest.GenerationParams.make({ maxTokens: 256 }) })
const turn = Effect.gen(function*() {
  return ModelEvent.settledMessage(yield* Stream.runCollect((yield* Model.Model).stream(request)))
}).pipe(Effect.provide(Route.layer(route).pipe(Layer.provide(RequestExecutor.layer), Layer.provide(FetchHttpClient.layer))))
```

### [@smthrs/std](https://std.smithers.sh)

Ships the seventeen tools a coding agent needs, read, edit, grep, bash and more, each a portable declaration plus a handler your host runs.

```ts
import { NodeServices } from "@effect/platform-node"
import * as Read from "@smthrs/std/Read"

const page = await Effect.runPromise(
  Effect.provide(Read.run({ path: "./README.md", limit: 20 }), NodeServices.layer)
)
console.log(page.content, page.totalLines, page.truncated)
```

### [@smthrs/memory](https://memory.smithers.sh)

Stores an agent's facts, notes, and message threads in SQLite so a later run can recall what an earlier one learned.

```ts
import { Flows, RecallKeyword } from "@smthrs/memory"
import * as TestMemory from "@smthrs/memory/test/TestMemory"

const rows = await Effect.runPromise(Effect.gen(function*() {
  yield* Flows.handlers.remember({ bank: "global-notes", key: "release", text: "cut 0.1.0" })
  return yield* Flows.handlers.recall({ banks: ["global-notes"], query: "release" })
}).pipe(Effect.provide(Layer.provideMerge(RecallKeyword.layer, TestMemory.layer))))
```

### [@smthrs/registry](https://registry.smithers.sh)

Lists what workflows a project contains by scanning files without running them, so a host can show them to a model and run one.

```ts
import * as NodeServices from "@effect/platform-node/NodeServices"
import { Executable, Registry } from "@smthrs/registry"

const registry = Executable.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)))
```

### [@smthrs/fs](https://fs.smithers.sh)

Turns a directory of flow files into named, schema checked commands that agents, shells, HTTP clients, and MCP tools can call.

`@smthrs/fs` is **workspace-private**: the example below runs inside this repository, not from an npm install.

```ts
import { Command, FileRouter } from "@smthrs/fs"

const program = Effect.gen(function*() {
  const { routes } = yield* FileRouter.scan({ root: "flows" })
  const commands = yield* Command.make(routes)
  return yield* commands.execute("review --number 42")
})
```

### [@smthrs/plugin](https://plugin.smithers.sh)

Lets a TypeScript program accept plugins from other people at named extension points, with typed hooks, Vite ordering rules, and a frozen config.

```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 } }
```

### [@smthrs/integrations](https://integrations.smithers.sh)

Connects a durable agent flow to GitHub, Linear, and Telegram with verified webhook ingress, typed clients, and replay-safe write actions.

```ts
import { GitHub } from "@smthrs/integrations"

const client = GitHub.GitHubClient.make({})

await Effect.runPromise(
  client.request("POST", "/repos/OWNER/REPO/issues/1/comments", { body: "Triaged." })
)
```

### [@smthrs/triggers](https://triggers.smithers.sh)

Starts workflows on a cron schedule or a verified webhook, so two hosts never fire the same scheduled run twice.

```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
})
```

### [@smthrs/evals](https://evals.smithers.sh)

Scores a flow against a fixed set of committed cases, compares each run with a saved baseline, and grades CI.

```ts
import { Baseline, Runner, Suite } from "@smthrs/evals"

const program = Effect.gen(function*() {
  const suite = yield* Suite.make({ name: "smoke", cases: [{ name: "hello", input: "hi" }], concurrency: 1 })
  const run = yield* Runner.run(suite, { runId: "ci-1", at: "2026-01-01T00:00:00.000Z" })
  return Baseline.write(yield* Baseline.fromRun(run))
})
```

### [@smthrs/scorers](https://scorers.smithers.sh)

Grades work an agent or flow already did, and keeps every score under a durable identity that survives restarts.

```ts
import { Scorer } from "@smthrs/scorers"
const exactMatch = Scorer.make({
  id: "docs/scorers/exact-match",
  version: "1",
  score: ({ groundTruth, output }) => Effect.succeed({ score: output === groundTruth ? 1 : 0 })
})
const graded = await Effect.runPromise(exactMatch.score({ input: "greet Ada", output: "Hi, Ada", groundTruth: "Hi, Ada" }))
```

## Durable execution

The engine underneath. How work is planned, journaled, replayed, cached, and sandboxed.

### [@smthrs/flows](https://flows.smithers.sh)

Runs long jobs as durable programs that record every finished step and resume where they stopped after a crash.

```ts
import { Action, Flow, Interpreter } from "@smthrs/flows"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
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)))
```

### [@smthrs/flow](https://flow.smithers.sh)

Declares durable workflows in TypeScript as named, typed steps, so a re-run replays what already finished instead of doing the work twice.

```ts
import { Action, Flow } from "@smthrs/flow"

const Summarize = Action.make("digest/Summarize", { payload: { url: Schema.String }, success: Schema.String })
const Digest = Flow.make("digest/Digest", {
  payload: { url: Schema.String },
  success: Schema.String,
  body: (payload) => Summarize.call(payload)
})
```

### [@smthrs/engine](https://engine.smithers.sh)

Executes durable flow programs, recording every step so a retried, resumed, or restarted run replays finished work instead of doing it twice.

```ts
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Node } from "@smthrs/plan"

const Build = Flow.make("build/Build", { payload: { target: Schema.String }, success: Schema.String, body: ({ target }) => Node.succeed(`${target}.js`) })
const layer = Interpreter.layer(Build).pipe(Layer.provideMerge(Action.layerImplementations), Layer.provideMerge(FlowEngine.layerMemory))
Effect.runPromise(Build.execute({ target: "app" }, { executionId: "build-1" }).pipe(Effect.orDie, Effect.provide(layer))).then(console.log)
```

### [@smthrs/engine-store](https://engine-store.smithers.sh)

Persists every step a flow takes to SQLite, so a crashed or killed run resumes where it stopped instead of restarting.

```ts
import { EngineStore } from "@smthrs/engine-store"

const engine = EngineStore.layer({
  owner: { hostId: "worker-a" },
  journalSource: "worker-a"
})
```

### [@smthrs/plan](https://plan.smithers.sh)

Turns declared steps into a durable graph where each step's identity is a hash of what it declares, so edits invalidate only affected work.

```ts
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { KeyMaterial, Plan } from "@smthrs/plan"

const material = { version: KeyMaterial.version, kind: "sealed", body: { pr: 4821 }, inputs: [], layers: [], capabilities: [] } as const
const nodes = [{ id: "read-pr", material, effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" } }] as const
const plan = await Effect.runPromise(Plan.compile({ planId: "review-4821", flow: "example/Review", nodes }).pipe(Effect.provide(NodeCrypto.layer), Effect.orDie))
console.log(plan.digest, plan.nodes[0].key)
```

### [@smthrs/journal](https://journal.smithers.sh)

Records what a long-running job did, in order, in an append-only SQLite history that scrubs credentials and survives a crash.

```ts
import { Journal, JournalEvent } from "@smthrs/journal"

const runId = "run-1" as JournalEvent.RunId
const program = Effect.gen(function*() {
  const journal = yield* Journal.Journal
  yield* journal.emitDurableUnfenced({ runId, sourceId: "engine" as JournalEvent.SourceId, eventType: "run.created", payload: { flow: "build" } })
  return yield* journal.entries({ runId, limit: 100 })
})
```

### [@smthrs/run-store](https://run-store.smithers.sh)

Keeps the live state of long running jobs in a SQL database and decides which process is allowed to touch each one.

```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")
})
```

### [@smthrs/step-cache](https://step-cache.smithers.sh)

Stores a finished step's result under a digest of its inputs so the next run reads it back instead of doing the work again.

```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))
```

### [@smthrs/time-travel](https://time-travel.smithers.sh)

Replays, forks, or rewinds a durable run from the journal it already wrote, so history can be read or branched without re-running anything.

```ts
import { Engine } from "@smthrs/flows"
import { TimeTravel } from "@smthrs/time-travel"
const forked = Effect.gen(function*() {
  const timeTravel = yield* TimeTravel
  const lineageId = Engine.FlowEngine.Lineage.root("build-42")
  return yield* timeTravel.fork({ runId: "build-42", frame: { lineageId, seq: 17 } })
})
```

### [@smthrs/patterns](https://smithers-patterns.smithers.sh)

Ready-made shapes for multi-step agent work, such as review loops and escalation ladders, that you can inspect before anything runs.

```ts
import { ReviewLoop } from "@smthrs/patterns"

const approved = await Effect.runPromise(ReviewLoop.run("Write the release notes.", {
  maxRounds: 3,
  produce: (goal: string) => Effect.succeed(`Draft: ${goal}`),
  review: (output: string) => Effect.succeed({ approved: output.includes("Draft") }),
  revise: ({ output }) => Effect.succeed(`${output} revised`)
}))
```

### [@smthrs/sync](https://smithers-sync.smithers.sh)

Replicates a run's recorded history to readers that do not own it, replaying what already happened and then following it live.

```ts
import { SyncClient } from "@smthrs/sync"
import { Effect, Stream } from "effect"

const follow = Effect.gen(function*() {
  const sync = yield* SyncClient.Sync
  const entries = sync.subscribe({ scope: { _tag: "Workspace" }, cursors: [] })
  yield* Stream.runForEach(entries, (entry) => Effect.logInfo(`${entry.seq} ${entry.eventType}`))
})
```

### [@smthrs/database](https://database.smithers.sh)

Runs every SQL write inside one transaction that retries lock conflicts, reports five stable error codes, and applies migrations from many packages in order.

```ts
import * as DurableWriter from "@smthrs/database/DurableWriter"
import * as SqlClient from "effect/unstable/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')`)
})
```

### [@smthrs/artifacts](https://artifacts.smithers.sh)

Stores bytes under their own SHA-256 address, so every read returns exactly the bytes that were published or fails loudly.

```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)
})
```

### [@smthrs/sandbox](https://sandbox.smithers.sh)

Runs an Effect program's files and commands on a machine you provision: a container, a microVM, a cloud sandbox, or a scratch directory. `DirectorySandbox` requires a lifecycle-backed spawner, such as `NodeHost.layerContained()` provided with a `ProcessLedger`; its [quickstart](https://sandbox.smithers.sh/quickstart/) shows the host composition.

```ts
import { DirectorySandbox, Sandbox } from "@smthrs/sandbox"
import * as FileSystem from "effect/FileSystem"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"

const write = Effect.flatMap(FileSystem.FileSystem, (fs) => fs.writeFileString("report.txt", "hi\n"))
const program = Effect.gen(function*() {
  const provider = DirectorySandbox.make({ fs: yield* FileSystem.FileSystem, spawner: yield* ChildProcessSpawner, root: "/var/tmp/smithers" })
  yield* Effect.scoped(Effect.provide(write, Sandbox.layerHost(provider, { session: "run:01J" })))
})
```

### [@smthrs/kernel](https://kernel.smithers.sh)

Checks every file, process, network, and repository operation an Effect program runs against a permission policy, refusing whatever nobody authorized.

```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))
```

### [@smthrs/capability](https://capability.smithers.sh)

Describes one operation a program is about to perform and decides from ordered rules whether it is allowed, denied, or needs a person.

```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"
```

### [@smthrs/keys](https://keys.smithers.sh)

Turns structured data into a stable content key, and validates keys read back from storage without hashing them again.

```ts
import { deriveKey } from "@smthrs/keys"

const key = await Effect.runPromise(
  deriveKey({ domain: "example/compile", version: 1, source: "main.ts" }).pipe(Effect.provide(NodeCrypto.layer))
)
// key1_74a286a394e4b0619c05801dd4e7315deeb83b8203cd3c3ee7cd6033ec55c683
```

### [@smthrs/crypto](https://crypto.smithers.sh)

Hashes text or bytes into a SHA-256 fingerprint with one strict format, from Effect code or plain synchronous code.

```ts
import { digest, digestSync } from "@smthrs/crypto"

const address = await Effect.runPromise(
  digest("hello").pipe(Effect.provide(NodeCrypto.layer))
)
const sameAddress = digestSync("hello")
```

### [@smthrs/canonical](https://canonical.smithers.sh)

Turns any JavaScript value into RFC 8785 canonical JSON, so equal data always produces the same bytes and the same hash.

```ts
import { canonicalize } from "@smthrs/canonical"
import { createHash } from "node:crypto"

const key = (value: unknown) => createHash("sha256").update(canonicalize(value), "utf8").digest("hex")

key({ a: 1, b: 2 }) === key({ b: 2, a: 1 })
// => true
```

### [@smthrs/core](https://core.smithers.sh)

Describes multi-step agent work as inert data and builds it into a graph you can inspect, order, and cache before anything runs.

```ts
import { Graph, Node } from "@smthrs/core"

const plan = Node.all({
  api: Node.dynamic({ model: "smart", prompt: "Review src/api.ts." }),
  cli: Node.dynamic({ model: "smart", prompt: "Review src/cli.ts." })
})
console.log(Graph.edges(Graph.build(plan)))
```

### [@smthrs/jj](https://jj.smithers.sh)

Snapshots and restores a working copy through Jujutsu version control, as one Effect service that runs on Node, Bun, or in a browser.

```ts
import { Jj } from "@smthrs/jj"
import * as NodeJj from "@smthrs/jj/node/NodeJj"
const program = Effect.gen(function*() {
  const jj = yield* Jj
  const { changeId } = yield* jj.snapshot("before the risky step")
  yield* jj.restore(changeId)
}).pipe(Effect.provide(NodeJj.layerAt("/srv/checkouts/main")))
```

### [@smthrs/observability](https://observability.smithers.sh)

Sends an Effect program's logs, metrics, and traces to an OpenTelemetry collector with one layer and no OpenTelemetry SDK.

```ts
import * as Otlp from "@smthrs/observability/Otlp"

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))
```

## Platform bindings

The same engine bound to a different runtime. Pick the one your host runs on.

### [@smthrs/platform-node](https://platform-node.smithers.sh)

Runs a program's side effects on Node.js through a filesystem, spawner, and HTTP client that a symlink or a crash cannot subvert.

```ts
import { NodeHost } from "@smthrs/platform-node"
import * as ChildProcess from "effect/unstable/process/ChildProcess"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make("git", ["status", "--short"]))
}).pipe(Effect.provide(NodeHost.layer))
```

### [@smthrs/platform-bun](https://platform-bun.smithers.sh)

Supplies a Bun program with one Effect layer for the five outside world services Smithers uses: files, paths, processes, version control, and HTTP.

```ts
import { BunHost } from "@smthrs/platform-bun"

const program = Effect.gen(function*() {
  const fs = yield* FileSystem.FileSystem
  yield* fs.writeFileString("report.txt", "written through the host\n")
})
Effect.runPromise(Effect.provide(program, BunHost.layer))
```

### [@smthrs/platform-browser](https://platform-browser.smithers.sh)

Gives a browser tab Effect's FileSystem and command spawning, backed by a virtual filesystem and a bash interpreter running in the page.

```ts
import { BrowserServices } from "@smthrs/platform-browser"
import { configureSingle, fs } from "@zenfs/core"
import { IndexedDB } from "@zenfs/dom"
import { Bash } from "just-bash"

await configureSingle({ backend: IndexedDB })
// One mount, two views: the interpreter's and the promises API the adapter reads.
const layer = BrowserServices.layer({ bash: new Bash({ fs }), fs: fs.promises })
```

## Standalone

Useful on their own, with or without the rest.

### [@smthrs/errors](https://errors.smithers.sh)

One error class with five fixed codes, so a caller classifies an integration failure by code instead of by matching message text.

```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)
}
```

### [@smthrs/testing](https://testing.smithers.sh)

Test library for Smithers flows: plan and journal assertions, engine conformance suites, and model doubles that record once and replay forever.

```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)))
  }
})
```

### [smthrs](https://smthrs.smithers.sh)

Holds the old npm name from Smithers 0.x and throws on import, naming the @smthrs/* packages that replaced the umbrella.

```bash
npm install --global @smthrs/cli@next
smthrs migrate
cat .smithers-migrate/report.md
```

---

# The smthrs command
https://smithers.sh/docs/reference/cli/

`smthrs` executes `PACKAGE.ts` target graphs and operates durable agent flows.
`smithers` is an alias of the same executable. A target is a declaration, a
flow is a workflow, and a run is a persisted flow execution.

## Commands

| Command | Purpose |
| --- | --- |
| `build/test/lint/docs/review/ci/run <pattern>` | Execute selected target kinds; `ci` combines build, test, lint, and docs. |
| `target <label>` or `//package:target` | Execute one exact target using its declared kind. |
| `targets [pattern]` | Discover available targets and summaries. |
| `show target <label>`, `show workspace`, `info` | Inspect target inputs, outputs, dependencies, and workspace configuration. |
| `query <expression>`, `graph [pattern]`, `owners <paths...>` | Query the graph, render dependencies, and resolve ownership. |
| `affected <verb> [pattern]` | Select changed work and dependents; `--list` previews the selection. |
| `watch <verb> [pattern]` | Replan and rerun when files change. |
| `explain <label>` | Show the planned key and local cache decision. |
| `flow list/show/plan/start/execute` | Discover workflows, plan them, start them, or execute approved payloads. |
| `runs list/show/logs/output` | Read durable run state, events, and outputs. |
| `runs cancel/cancel-all/resume/signal/steer` | Operate existing durable runs. |
| `runs inspect/replay/fork/rewind` | Inspect historical frames, branch a run, or restore an earlier frame. |
| `approvals list/approve/deny` | Read pending approvals and submit the exact payload or `@file`. |
| `init [name]`, `generate app/flow/package/ci` | Set up a workspace or scaffold a resource. |
| `create-app [directory]` | Scaffold an app from a `@smthrs/create-app` template. |
| `install`, `git-hooks [--write]` | Apply the workspace's declared installation and hook configuration. |
| `cache status/prune/clear`, `clean`, `gc` | Maintain local action results, declared cleanup targets, and old runs. |
| `memory`, `credentials`, `triggers`, `integrations`, `eval` | Operate persistent agent features. |
| `serve`, `doctor`, `suggest`, `migrate`, `update`, `bug` | Host, diagnose, discover uses, migrate source, check versions, or report a bug. |

Target patterns include `//...`, `//package/...`, and `//package:target`.
Use `--plan` to preview execution, `--jobs` to bound concurrency, and
`--no-cache` to bypass result reuse. `affected` defaults to comparing `HEAD`
against the working tree, including untracked files; `--base`, `--head`, and
`--files` select other inputs. Ambient configuration changes conservatively
select more work.

Top-level [`run`](/docs/reference/cli/run/) executes run-kind targets.
`flow start` plans, approves, and starts a durable workflow; `flow execute`
submits a previously approved plan. `runs resume` re-drives a parked run.

## Operator groups

| Group | Available commands |
| --- | --- |
| [`memory`](/docs/reference/cli/memory/) | `list/get/set/rm`, `recall`, `notes list/get/add/status/supersede`, `threads list/create/show/rm`, `messages list/add`, `compact`. |
| `credentials` | `list/add/rotate/revoke`; only references appear in output. |
| `triggers` | `list/show/register/enable/disable/fire/serve`; `fire` queues an occurrence for a running scheduler. |
| `integrations` | `list`, `doctor [--offline]`, and GitHub webhook `reconcile`. |
| `eval` | `list/run/baseline/compare` for modules under `evals/**/*.eval.ts`. |

Credentials use `SMITHERS_CREDENTIAL_KEY`, a base64-encoded 32-byte host key.
Supply secrets through `--secret-env` or `--secret-file`; keep the host key
outside the database. Integrations read `.smithers/integrations.json` or
discover configured provider environment variables. Configuration entries name
an `id`, `provider`, and optional `tokenEnv` or `credentialId`. GitHub webhook
reconciliation reads `.smithers/listeners.json`, plans by default, writes with
`--apply`, and requires `--allow-delete` to delete owned hooks.

Both `serve` and `triggers serve` host the durable scheduler. Trigger dispatch
preserves flow approval requirements; disabling a trigger does not cancel an
active run. `triggers show <id>` exposes `activePlan.plan.approval`; submit that
unchanged to `approvals approve '<payload>' --scope run`. Waiting plans survive
restarts without timing out or changing their approval payload. `approvals list`
lists in-run requests, not these pre-run plans.

## Project selection and output

For scripts, target commands and `generate ci/package` document `--workspace`
in their schemas but the executable also accepts `--root` as an alias;
flow and operator commands use `--root`.
Flow control, normal run management, and approvals also accept `--remote`
and `--credential`, with `SMITHERS_REMOTE` and `SMITHERS_API_KEY` as fallbacks.
History, memory, triggers, credentials, integrations, evaluations, and local
maintenance require the machine that owns the selected workspace.

Incur parses the public CLI using Zod schemas; Effect supplies the execution
runtime, and Clack supplies live human progress and interactive prompts. `--help` and `--schema`
describe the installed command. Canonical commands use Incur's `--json` and
`--format` output; `--format jsonl` is useful for event streams. Compatibility
aliases retain their earlier output shapes.

### Human and agent output

Agent harness markers select concise Incur output even in a PTY; humans see
task progress on standard error. CI and pipes use conservative machine output.
Override with `--audience auto|human|agent` or `SMITHERS_AUDIENCE`.

`--silent` suppresses progress, not results or failures. `--quiet` is retained
where previously supported; use `--silent` across groups. Agents can use
`--verbose` for plain progress or request
stored logs with `smthrs runs logs <run-id> --follow --format jsonl`.
`--json` preserves machine-readable standard output while human progress stays
on standard error. MCP always remains machine-clean.

Agent log pulls without `--follow` default to 100 events and incremental JSONL.
Set `--limit 1..10000` to adjust the page size, then use the returned next command
with `--after <sequence>`. `--follow` streams without the default 100-event cap;
an explicit `--limit` still applies.

The shared `Audience` utility centralizes detection and presentation policy;
see the [verified harness registry](https://github.com/smithersai/smithers/blob/main/packages/smithers/build/build-cli/docs/reference/agent-detection.md)
for evidence and known gaps. Detection never changes permissions or approvals.

## Storage and maintenance

`.flows/control.db` holds control state, memory, credentials, and triggers;
`.flows/engine.db` holds execution history. Historical `inspect` and `replay`
read committed frames without calling models or re-executing actions.

`runs fork <run> --at <sequence>` requires an eligible parked or terminal
agent run, an approved plan, and `jj`; its isolated workspace is retained in
`.flows/forks/`. Resume the returned child with `runs resume`. Rewind uses
`--at <sequence> --preview` to inspect the affected suffix, then `--yes` to
archive it and restore the frame of a pending or suspended engine run. Use
`fork` for terminal history; active runs and unsafe effect boundaries are refused.

Evaluation runs are saved in `.flows/evals/runs/`; baselines default to
`evals/<encoded-suite>.baseline.json` and require `--force` to overwrite.
Comparison exits 1 for a regression and 5 for inconclusive results.

The target result cache normally lives in `.flows/cache/`, under the
workspace's configured cache directory. `cache prune/clear --dry-run`
previews individual local result files; deletion requires `--yes` and leaves
run databases, remote entries, and artifact blobs alone. `clean` runs
declared `Clean` targets. `gc --dry-run` previews old terminal-run retention.

## Compatibility

Flat commands such as `up`, `ls`, `ps`, `status`, `logs`, `output`, `plan`,
`approve`, `deny`, `cancel`, `signal`, `steer`, and `down` remain hidden
transition aliases. Approved JSON payloads passed to `run`, and `run --resume`,
also retain their old handler. Use the canonical groups for new scripts.
The Claude mirror protocol is hidden as `internal claude`.

[`up`](/docs/reference/cli/up/) · [`ls`](/docs/reference/cli/ls/) · [`plan`](/docs/reference/cli/plan/) · [`ps`](/docs/reference/cli/ps/) · [`status`](/docs/reference/cli/status/) · [`logs`](/docs/reference/cli/logs/) · [`output`](/docs/reference/cli/output/) · [`cancel`](/docs/reference/cli/cancel/) · [`down`](/docs/reference/cli/down/) · [`signal`](/docs/reference/cli/signal/) · [`steer`](/docs/reference/cli/steer/) · [`approve`](/docs/reference/cli/approve/) · [`deny`](/docs/reference/cli/deny/).

Ordinary success exits 0; failures and Incur argument errors exit 1. Legacy
usage errors and explicit confirmation refusals use 2; flows awaiting
approval use 3. Inconclusive evaluations exit 5. Interruption
and termination use 130 and 143. See [environment variables](/docs/reference/environment-variables/)
for the control-plane configuration.

---

# Triggers
https://smithers.sh/docs/reference/triggers/

A trigger is a durable cron declaration that starts one flow with fixed input. This page records what the released packages do: the declaration schema, the `smthrs triggers` verbs, how the scheduler claims and fires occurrences, and the outcomes it records. The [Planned](#planned-event-selectors-and-automations) section at the end records the repository-event selector design, which is not built. For a walkthrough, see [Define triggers](/docs/guides/triggers/). For every exported symbol, see [`@smthrs/triggers`](/docs/reference/api/triggers/).

## Declaration

`Trigger.make` from `@smthrs/triggers/Trigger` decodes and validates one declaration. `smthrs triggers register` calls it on the flags or JSON file you pass.

| Field | Requirement | Behavior |
| --- | --- | --- |
| `id` | Required, non-empty | The trigger's identity. Registering the same id replaces the row and increments its revision. |
| `flowId` | Required, non-empty | The flow the scheduler starts. The triggers package never resolves it; Control does at launch. |
| `input` | Default `{}` | The flow's input as JSON. `undefined`, `NaN`, a `Date`, or a function is refused as `invalid_trigger` with `path: "input"`. |
| `cron` | Required | A five-field cron expression, minute precision. Refused as `invalid_cron` when malformed and `unsatisfiable_cron` when the calendar never satisfies it. |
| `timezone` | Optional, default UTC | An IANA timezone name. |
| `overlap` | Default `"skip"` | `"skip"`, `"buffer-one"`, or `"supersede"`. Applied when an occurrence is due while the trigger's run is active. |
| `catchUp` | Default `"none"` | `"none"`, `"one"`, or `"all"`. Applied to occurrences missed while no scheduler ran. |
| `maxCatchUp` | Default `0`, at most `1000` | The most missed occurrences one catch-up replays. |
| `enabled` | Default `true` | A disabled trigger is listed and never claimed. |

The satisfiability probe searches forward from the current instant for one occurrence, the same search every tick performs, so `0 0 30 2 *` is refused where it is written rather than at the tick that would have fired it. Shape failures name the offending field in `TriggerError.path`.

## CLI verbs

All verbs run against the nearest Smithers project, or the one named by `--root`. They store rows in `.flows/control.db` and print JSON.

| Verb | Effect |
| --- | --- |
| `smthrs triggers list` | Every registration ordered by id, with `revision`, `enabled`, and `lastFiredAt` once an occurrence has been recorded. |
| `smthrs triggers show <id>` | The registration, `activeRun` (a run id or launch reservation, or `null`), and `activePlan` with the exact `plan.approval` payload when the run is waiting for approval. |
| `smthrs triggers register [id] --flow <flowId> --cron <expr> [--input <json>] [--timezone <iana>] [--overlap <policy>] [--catch-up <policy>] [--max-catch-up <n>] [--enabled]` | Validates and upserts from flags. |
| `smthrs triggers register <id> --file <path>` | Validates and upserts from a JSON declaration relative to the project root. The file's `id` must equal the argument. |
| `smthrs triggers fire <id> [--occurrence <unix-ms>]` | Queues one pending occurrence for a running scheduler. Refuses a disabled trigger. `--occurrence` fixes the retry identity; the default is now. |
| `smthrs triggers enable <id>`, `smthrs triggers disable <id>` | Flip future occurrences and increment the revision. Neither cancels an active run. |
| `smthrs triggers serve [--poll-interval-ms <n>]` | Runs the scheduler until interrupted. Default poll interval 1000 ms. |

`smthrs serve` hosts the same scheduler beside the control server, so a project needs one of the two processes for schedules to fire. Every verb refuses `--remote`: triggers are administered on the host that owns the project.

## Scheduler

The scheduler is `Scheduler.layer` from `@smthrs/triggers/Scheduler` over the SQL trigger store. The CLI supplies the durable Control-backed `TriggerPlans` runner.

On each tick it lists enabled triggers, computes the occurrences due since each trigger's `lastFiredAt` under its catch-up policy, and claims each one in the store. The claim reads the trigger row's current policy and revision inside the same transaction, so a scheduler holding a stale snapshot cannot fire a trigger that was disabled or repointed since it last looked. Claim results are `skip`, `buffer`, `fire`, or `supersede`; only `fire` and `supersede` hand out a reservation, and `supersede` also names the run it displaces.

A fired occurrence starts the flow through Control with the idempotency key `<triggerId>:<occurrence ISO instant>`. Two schedulers that notice the same boundary derive the same key, and a retried `fire` with the same `--occurrence` reuses it, so one occurrence never becomes two runs.

CLI approval plans remain active across scheduler restarts with the same payload and no attempt counter or expiry. The scheduler never approves them. See [Resolve a parked approval](/docs/guides/triggers/#resolve-a-parked-approval) for listing triggers, inspecting a plan, approving it, or explicitly cancelling the parked launch with `smthrs approvals deny`.

:::note[Library embedders only]
`Scheduler.layerControlRunner` has a bounded approval retry policy. Its limit is documented in [Resolve a parked approval](/docs/guides/triggers/#resolve-a-parked-approval).
:::

Stopping the scheduler cancels nothing. Runs are durable; the next scheduler re-attaches to active runs from the store. Only a `supersede` claim cancels a run.

A catch-up that owes more occurrences than `maxCatchUp` logs a warning annotated with the trigger id, abandons the backlog beyond the bound, and still fires the current occurrence. The bound is a statement about how much history to replay, not a reason to stop scheduling.

## Fire ledger

`recordResult` writes one outcome per occurrence:

| Outcome | Meaning |
| --- | --- |
| `launched` | Control accepted the run; it is active. |
| `completed` | The run settled. |
| `skipped` | Due while a run was active under `skip`. |
| `buffered` | Waiting behind an active run under `buffer-one`. A later due occurrence coalesces into the same buffer. |
| `superseded` | Cancelled the active run under `supersede`. |
| `failed` | The runner could not prepare or launch the flow. `error` carries the reason. |

`lastFiredAt` advances on skipped and buffered occurrences as well as fired ones, so it is the catch-up cursor and not the occurrence an active run owns. `activeOccurrence` answers that question; `smthrs triggers show` prints the active run id.

The ledger has no CLI or app listing yet. Rows are readable in `.flows/control.db`.

## Reading triggers in the app

The `triggers.list` flow (slash command `/triggers.list`, or ask the agent to list the triggers on a repository) renders the Dispatcher card: one row per trigger with the schedule in words, the flow it runs, `enabled` or `disabled`, and the last fire time. The card is read-only; registering and editing happen through the CLI.

The card reads the workspace gateway. A gateway that does not serve its trigger store answers with a reason, and the card prints that reason verbatim instead of an empty list. Smithers Cloud workspaces do not serve the store yet, so the card reports that today. Signed-out visitors get the sign-in door.

## Webhooks

`Webhook.make` from `@smthrs/triggers/Webhook` builds a verified door in code. It requires a credential reference, verifies the signature header over a private copy of the raw request bytes in constant time, refuses an absent or empty signature before computing anything, and hands the decoded payload to Control's channel coordinator. It exposes `register` and `ingest` and no direct execution method. `ingest` does not register the channel; run `register` first.

There is no `smthrs webhooks` verb, no persisted webhook registry, and no webhook row in the Dispatcher card. For the module contract, see [`@smthrs/triggers`](/docs/reference/api/triggers/#webhook).

## Planned: event selectors and automations

This section describes work that is not built. `@smthrs/targets` exports no `S.Automation` and `S.Package` accepts no `automations` entry. `@smthrs/triggers` exports no `Trigger.push`, `review`, `tag`, `schedule`, `manual`, `event`, or `succeeded`. The type-level proof of the intended surface is [`apps/site/examples/cloud-triggers/contract.d.ts`](https://github.com/smithersai/smithers/blob/main/apps/site/examples/cloud-triggers/contract.d.ts); its README states that it does not implement trigger registration, matching, dispatch, or the `S.Automation` runtime, and that the runtime APIs behind these signatures still need to be built.

The design binds typed event selectors to one target or flow with `S.Automation({ on, run, when?, input? })`, exported through `Package.automations` so a root entry `automations: { ci }` has the label `//:ci`.

| Planned constructor | Options | Event fields beyond `id`, `repository`, `receivedAt` |
| --- | --- | --- |
| `T.push(options?)` | `branches?`, `paths?` | `type: "push"`, `branch`, `commit`, `before`. |
| `T.review(options?)` | `actions?`, `base?`, `head?`, `paths?`, `drafts?` | `type: "review"`, `action`, `review`; labeled events also carry `label`. |
| `T.tag(options?)` | `names?` | `type: "tag"`, `name`, `commit`. |
| `T.schedule(options)` | `cron`, `timezone?`, `ref?` | `type: "schedule"`, `scheduledAt`, `timezone`. The declared form of today's cron trigger. |
| `T.manual(options?)` | `input?` schema | `type: "manual"`, decoded `input`. |
| `T.event(options)` | literal `name`, `input` schema | `type: "event"`, `name`, decoded `input`. |
| `T.succeeded(automation)` | an automation reference | `type: "succeeded"`, `runId`, `commit`, `cause`, typed `output`. |

Planned matching rules: entries in `on` combine with OR; fields within one selector combine with AND; values in a filter list combine with OR; an exclusion wins over every inclusion; `when` must return `true` after a selector matches. Planned delivery rules: subscriptions register from the default branch or bookmark, each accepted event records the subscription revision separately from the execution revision, and redelivery returns the first receipt.

None of those rules run anywhere today. When they land, this section moves above the fold and the shipped cron trigger becomes `T.schedule`.

---

# Troubleshooting
https://smithers.sh/docs/troubleshooting/

Find your symptom verbatim below. Each entry names the cause, the fix, and the command or output that verifies the fix held.

  - [unsupported_database](/docs/troubleshooting/unsupported-database/): The CLI refuses SMITHERS_BACKEND or --backend values other than sqlite.
  - [Exit code 3: waiting-approval](/docs/troubleshooting/waiting-approval/): A run parked on an approval exits 3 by design; approve the payload and resume.
  - [A run is stuck](/docs/troubleshooting/stuck-run/): Diagnose a run with no live owner, or a park nobody understands.
  - [doctor reports a provider problem](/docs/troubleshooting/doctor-provider/): Fix the provider keys the smthrs doctor providers check warns about.
  - [Error reference](/docs/reference/errors/): The structured errors the CLI and engine report, and what each one means.

---

# A run is stuck
https://smithers.sh/docs/troubleshooting/stuck-run/

Start by reading the run's status and recent events. Replace `<run-id>` with the run ID from `smthrs runs list`:

```bash
smthrs runs show <run-id> --format json
smthrs runs logs <run-id> --limit 100 --format jsonl
```

Elapsed time alone does not prove the executor died. A model call, a durable timer, and a human approval can all leave a run quiet.

## The run is waiting for approval

Read the recorded request and use [Waiting for approval](/docs/troubleshooting/waiting-approval/) to submit its exact payload. Resuming without answering the request can leave the run waiting at the same point.

## The run is parked on a signal or timer

Check the waiting reason and the last recorded events. A signal wait needs the expected signal and payload; a timer needs its deadline to pass. Follow [Durable waits](/docs/guides/durable-waits/) for those contracts.

## The driving process stopped

If the host or process stopped, start a live driver against the same project state and resume the execution:

```bash
smthrs runs resume <run-id>
```

Ownership checks prevent the new process from taking work that another live owner still holds. A fresh lease or positive liveness probe can defer recovery. Do not edit owner rows to bypass those checks.

**Verify:** the run advances and new events appear. If takeover is refused, inspect the ownership evidence and host liveness. [Execution IDs and ownership](/docs/concepts/ownership/) explains the lease and fencing behavior.

## The run was accepted but has no executor

An admission receipt proves that the control plane accepted work. It does not prove that a host can execute the flow. Check the run's diagnosis for a missing seat, missing capabilities, or a flow registered on a different host.

Run `smthrs doctor` on the execution host and check its flow catalog. Correct the host configuration before launching again. Cancel an abandoned run with `smthrs runs cancel <run-id>` when you no longer intend to execute it.

## Collect a report

Keep the run ID, CLI version, diagnosis, and relevant events. The [`smthrs bug` reference](/docs/reference/cli/bug/) explains how to submit a report and which diagnostic data it includes.

---

# Exit code 3: waiting-approval
https://smithers.sh/docs/troubleshooting/waiting-approval/

Exit `3` from a local attached launch means the run parked at `waiting-approval`. The state is durable: answering the request and resuming can continue the same execution.

## Find the request

Replace `<run-id>` with the ID from the launch receipt:

```bash
smthrs runs show <run-id>
smthrs approvals list
```

For an in-run request, use the exact payload recorded with the approval request. The pending approval listing covers in-run requests. A plan that has not launched yet carries its approval object in the `flow plan` result instead.

Do not construct an approval from only the run ID or copy a payload from another attempt. The decision binds to the request's target and identity.

## Approve and resume

Save the request payload as `approval.json`, review what it authorizes, then submit it:

```bash
smthrs approvals approve @approval.json --scope run
smthrs runs resume <run-id>
```

The default scope on `approvals approve` is `once`; the command above chooses `run` explicitly. To refuse the request, use `smthrs approvals deny @approval.json`.

**Verify:** `smthrs runs show <run-id>` reports progress beyond that request. The flow can later ask a different question, so a new `waiting-approval` state needs its own inspection.

## The decision did not unblock the run

Check that you selected the same project or remote host, the same run, and the current request. Read the command's refusal before retrying. A stale or already-resolved request must not be treated as a new approval.

For the full admission sequence, see [Plan, approve, and run](/docs/guides/plan-approve-run/). In CI, a parked run needs state that outlives the runner; see [Run flows in CI](/docs/guides/ci/).

---

# Fix provider readiness
https://smithers.sh/docs/troubleshooting/doctor-provider/

If an agent flow cannot resolve its model seat, check the seat declaration and its credential separately.

## Check the declared seat

Read the flow's `model` field, or the `seat` passed to `AgentAction.make`. The provider prefix selects the credential and resolver. For example, `openai:gpt-5.4` uses the OpenAI route; an Anthropic key does not authenticate that route.

[Configure model seats](/docs/guides/model-seats/) lists the supported prefixes and their environment variables, including Cerebras, Gemini, and Moonshot.

## Check readiness

Run the diagnostic from the project root:

```bash
smthrs doctor
```

An unset or empty credential can produce a provider warning. The diagnostic checks credential presence; it does not prove that a key is valid, the account has quota, or the requested model is available. Its provider list may also cover fewer routes than the seat resolver.

Set the credential for the selected route in the shell that will launch the executor:

```bash
export OPENAI_API_KEY="<your API key>"
```

For a detached executor or a control-plane server, set the credential in that process's environment before starting it. Changing a different terminal's environment does not update a running process.

## Distinguish the failure

| Symptom | Check next |
| --- | --- |
| No seat resolves | Provider prefix, model name, credential presence, and authentication mode |
| Provider rejects authentication | Key validity and account access |
| Provider reports quota or capacity | [Quota policy](/docs/guides/agent-policies/); the run may park until a recorded deadline |
| Doctor warns about registry entries | Flow frontmatter and the reported source path |

After fixing the configuration, start the flow and inspect its actual result:

```bash
smthrs flow start reviewer
smthrs runs show <run-id>
```

A successful readiness check is useful preparation. A completed model call verifies the route and credential together.

---

# unsupported_database
https://smithers.sh/docs/troubleshooting/unsupported-database/

## unsupported_database

```text
unsupported_database: 1.0.0-rc.0 supports local SQLite only. PostgreSQL and PGlite are not available. Unset SMITHERS_BACKEND or set it to sqlite. See https://smithers.sh/migration/1.0#databases
```

**Cause.** `SMITHERS_BACKEND` or `--backend` names `pglite` or `postgres`. Storage in 1.0.0-rc.0 is local SQLite only, and the command exits 1 rather than silently running your flows against a database you did not ask for.

**Fix.** Unset `SMITHERS_BACKEND`, or set it to `sqlite`. If you passed `--backend pglite` or `--backend postgres`, drop the flag or pass `--backend sqlite`:

```bash
unset SMITHERS_BACKEND
export SMITHERS_BACKEND=sqlite
```

A different one-line stderr notice fires when a 0.x deployment's connection strings are still exported (`SMITHERS_TEST_PG_URL` or any `SMITHERS_POSTGRES_*` name):

```text
ignored: SMITHERS_POSTGRES_URL has no effect in 1.0.0-rc.0 (SQLite only)
```

That line is a notice, not a refusal: it changes no exit code and no result. It exists because a silently ignored connection string is how a project ends up running against SQLite while believing it runs against PostgreSQL.

**Verify.** The command proceeds past backend selection and works against the project's SQLite state under `.flows/`.

## Still stuck?

Run `smthrs bug` with the symptom as the summary. [smthrs bug](/docs/reference/cli/bug/) posts the report with your CLI version, platform, and Node version attached, and everything passes through the journal's shared redaction rules before it leaves the machine.

---

# Compatibility policy
https://smithers.sh/docs/migration/compatibility/

The release policy freezes this wording:

> Smithers 1.0.0-rc.0 is a source migration, not a compatible upgrade. It provides no JSX workflow API, no `smthrs/jsx-runtime` or `smthrs/jsx-dev-runtime`, no React reconciler, no `<Workflow>`, `<Task>`, `<Sequence>`, `<Parallel>`, `<Loop>`, `<Ralph>`, `<Branch>`, `<Approval>`, `<Signal>`, `<Timer>`, `<Subflow>`, `<Worktree>`, or `<Saga>` components, no `createSmithers`, `runWorkflow`, `renderFrame`, or `SmithersCtx`, no `smthrs` facade, no 0.x CLI verbs beyond those listed in the 1.0 command table, no 0.x gateway protocol, and no ability to load, resume, or migrate 0.x run databases. No shim, adapter, or compatibility layer will be published. Flows are written against `@smthrs/flow` (`Flow`, `Action`, durable waits, `RetryPolicy`), `@smthrs/engine`, `@smthrs/control`, and Effect `4.0.0-rc.112`, and run on Node.js 22.19.0 or later with local SQLite. Existing 0.x projects migrate their source with the `migrate-smithers-v1` workflow (`smthrs migrate`), which rewrites workflows, imports, configuration, scripts, and docs and reports every construct it could not translate. Runtime behavior between 0.x and 1.0 is not equivalent and is not intended to be.

> Storage in rc.0 is local SQLite only. PostgreSQL and PGlite are unsupported: `SMITHERS_BACKEND=pglite|postgres` and `--backend pglite|postgres` exit with `unsupported_database`.

## What this means in practice

- rc.0 has no JSX workflow API and no React reconciler. None of the 0.x components exist, and `smthrs/jsx-runtime` and `smthrs/jsx-dev-runtime` are gone.
- rc.0 has no `smthrs` facade. `createSmithers`, `runWorkflow`, `renderFrame`, and `SmithersCtx` do not exist.
- rc.0 does not load, resume, or migrate 0.x run databases. `smthrs doctor` and `smthrs migrate` open a 0.x `smithers.db` read-only so they can name runs that have not finished; nothing resumes them. See [Upgrade from 0.x to 1.0](/docs/migration/1.0/).
- No shim, adapter, or compatibility layer will be published.
- Runtime behavior between 0.x and 1.0 is not equivalent and is not intended to be.

## What is preserved

- The 0.x verb names refuse with exit 1 and a sentence that names the replacement and links into the migration guide. A removed verb never fails as a usage error, and neither does a removed flag; the flag refuses on the command that used to carry it.
- `smthrs migrate` rewrites 0.x project source to the 1.0 authoring model and reports every construct it could not translate. See [Upgrade from 0.x to 1.0](/docs/migration/1.0/).
- Two spellings survive as aliases: bare `smthrs gateway` runs `smthrs serve`, and `smthrs workflow list` runs `smthrs ls`.

## Storage

rc.0 keeps run state in local SQLite under `.flows/` and nowhere else. PostgreSQL and PGlite are unsupported: `SMITHERS_BACKEND=pglite` or `SMITHERS_BACKEND=postgres`, and `--backend pglite` or `--backend postgres`, exit 1 with `unsupported_database`. `--backend sqlite` names the supported backend and is accepted as a no-op. See [Unsupported database](/docs/troubleshooting/unsupported-database/).

## Runtime requirements

Flows written for 1.0 run on:

- Node.js 22.19.0 or later.
- Effect exactly `4.0.0-rc.112`. The pin is exact, not a range; the platform package enforces it through peer dependencies.
- The `@smthrs/flow`, `@smthrs/engine`, and `@smthrs/control` packages, installed at the `@next` dist-tag. `latest` still resolves 0.x; see [Installation](/docs/installation/).

## See also

  - [Upgrade from 0.x to 1.0](/docs/migration/1.0/): Migrate a 0.x project with smthrs migrate and look up every removed command and flag.
  - [Error reference](/docs/reference/errors/): The errors the CLI reports, including unsupported_database.

---

# Upgrade from 0.x to 1.0
https://smithers.sh/docs/migration/1.0/

Smithers 1.0.0-rc.0 is a source migration, not a compatible upgrade. A 0.x project built on JSX workflows or a `.smithers` pack becomes a `flows/**` directory of `flow.mdx`, `flow.ts`, and `SKILL.md` files. The `smthrs` facade and the JSX runtime are gone: rc.0 ships no shim, and no shim, adapter, or compatibility layer will be published. Run databases do not carry over either, because rc.0 never loads, resumes, or migrates 0.x run state. The `smthrs migrate` command rewrites your source one unit at a time and reports every construct it could not translate.

## What maps to what

| 0.x construct | 1.0 equivalent |
| --- | --- |
| `<Workflow>` and the other JSX components | A module flow at `flows/<name>/flow.ts`, built on `Flow.make(tag, { payload, success, error, body })`. The JSX root has no counterpart; the flow declaration is the root. |
| Markdown agent steps (MDX prompts) | `flows/<name>/flow.mdx`, or a `prompt: (payload) => string` template on an agent action. `{props.x}` becomes `${payload.x}`. |
| `createSmithers` and `runWorkflow` | `Flow.make(tag, { payload, success, error, body })` and `flow.execute(payload, { executionId })`, run by a host that registers the flow and provides its action implementations and engine. The CLI discovers and plans module flows, but leaves their execution `pending` until a host drives them. `smthrs up` executes markdown agent flows directly. |
| `.smithers/workflows/` | `flows/`. A workflow keeps its position: `.smithers/workflows/pipelines/ci-fast.tsx` becomes `flows/pipelines/ci-fast/flow.ts`. |
| `smithers.config.ts` | No 1.0 equivalent. `repoCommands.test` seeds the migration's test command (override it with `--verify-test`), `backend` is recorded in the report, and the file is deleted after the final unit. |
| `smithers` binary | Both spellings still work. `@smthrs/cli@next` installs `smthrs`, with `smithers` as an alias of the same executable. |

The construct-level mapping the tool applies, hundreds of rows covering every export of the 0.x facade, ships inside `@smthrs/migrate` and is recorded row by row in the migration report.

## Before you migrate

- Finish or archive every non-terminal 0.x run with the 0.x CLI. `bunx smthrs@0.35.0 ps` lists them. rc.0 never loads 0.x run state, and `smthrs migrate` refuses a project whose `smithers.db` still holds runs that have not finished.
- Commit your work with jj or git. `smthrs migrate --apply` checkpoints every unit before it edits anything, as a jj change, a git ref, or a file copy, and a clean working copy gives each migration unit a checkpoint you can restore. A project under no version control refuses the run unless you pass `--allow-no-vcs`.

## Scan the project

```bash
smthrs migrate --scan
```

The scan is an inventory only. It reads the project and writes nothing: packages, lockfiles, imports, pragmas, tsconfig chains, workflow and prompt files, components, tests, libraries, scripts, config, integrations, and any 0.x run state. Read what it found before you plan.

## Plan and apply

With no flags, `smthrs migrate` plans: it scans, plans the migration units, and writes `.smithers-migrate/report.json` and `.smithers-migrate/report.md`. It changes nothing else.

```bash
smthrs migrate
```

To convert the project source, pass `--apply` and name the model seat the migration's agent runs on:

```bash
smthrs migrate --apply --seat anthropic:<model>
```

An apply run handles one unit at a time:

1. Checks the two gates. A refused gate exits 3 with the project untouched; see [The two gates](#the-two-gates) below.

2. Checkpoints the unit: a jj change, a git ref, or a file copy, plus a digest of the whole tree, taken before the unit edits anything.

3. Rewrites the unit with a model-backed agent on the seat `--seat provider:model` names. A run with no seat and no provider key refuses by name instead of guessing.

4. Verifies the unit: install, format, typecheck, tests, and registry discovery. A failed round is handed back for repair up to `--max-repair-rounds` times (3 by default); a unit that still fails is restored from its checkpoint and reported as failed, and the next unit runs.

5. Archives the sources the migration replaced to `.smithers-migrate/archive/` (`--keep-old-sources` leaves them in place) and rewrites `package.json`, `tsconfig*.json`, and `.gitignore` where they are.

6. Writes `.smithers-migrate/report.json` and `.smithers-migrate/report.md` (`--report-dir` moves them).

The run exits 0 when every planned unit finished, 1 when a unit failed and was restored from its checkpoint, and 3 when it parked at a gate. Commit `report.md`: it records what the tool changed, what it could not translate, and what a person still has to decide.

:::caution
Read the verification output in the report before you commit it. Each command's captured output is rendered verbatim, and the tool does not redact it: a failing install or test suite in a 0.x project prints whatever it prints, secret or not.
:::

## The two gates

Two gates stand between the scan and any edit. Both refuse `--apply` with exit 3 and leave the project untouched, because each is a decision only a person can make.

1. **Run state.** A project that still holds 0.x run state cannot be migrated, because the 1.0 runtime can neither read nor resume it. That is true whether the runs are live, parked, or finished, so both the `blocked` and the `history-only` verdict refuse. To accept the run state the report lists and migrate the source anyway, pass `--acknowledge-run-state`. The tool never writes under any recorded run-state path.
2. **Unsafe constructs.** A construct with no safe translation has no honest automatic rewrite. To accept one, pass `--allow-unsafe <name,...>`; to accept every one the scan found, pass `--allow-unsafe all`. Even then the agent leaves a `TODO(migrate-smithers-v1)` marker and an `unsupported` report entry rather than an imitation.

Before either gate, `smthrs migrate` checks every `smithers.db` beside the project. A database that holds runs which have not finished, or one that cannot be opened, refuses the command with exit 1 and lists each run with its id, status, and workflow name. No flag releases this refusal. Finish, archive, or discard the runs with the 0.x CLI (`bunx smthrs@0.35.0 ps`), then run the command again. See [run-data](#run-data) below.

## Flags

`smthrs migrate` and the standalone `smithers-migrate` bin share one entry point and take the same options.

| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--scan` | boolean | `false` | Inventory the project and write the report without planning any unit. |
| `--apply` | boolean | `false` | Convert the project source, instead of planning the conversion. |
| `--seat` | string | none | The model seat the migration's agent runs on, as `provider:model`. |
| `--allow-unsafe` | string | none | Accept the named unsafe constructs, or `all`. |
| `--acknowledge-run-state` | boolean | `false` | Accept the 0.x run state the report lists and migrate the source anyway. |
| `--allow-no-vcs` | boolean | `false` | Accept a file copy as the only checkpoint, in a project under no version control. |
| `--keep-old-sources` | boolean | `false` | Leave the 0.x sources in place beside the flows written from them. |
| `--unit` | string | none | Migrate only these units, comma separated. |
| `--max-repair-rounds` | integer | `3` | How many times one unit may be repaired before it is reported as failed. |
| `--report-dir` | string | `.smithers-migrate` | Where the report is written, relative to the project root. |
| `--flows-dir` | string | `flows` | Where the written flows go, instead of `flows/`. |
| `--verify-install` | string | derived from the lockfile | The command that installs dependencies, instead of the one the lockfile implies. |
| `--verify-format` | string | derived from the project config | The command that formats the project, instead of the one its config implies. |
| `--verify-typecheck` | string, repeatable | derived, one per `tsconfig.json` | The command that typechecks the project, repeatable; one empty value runs no typecheck at all. |
| `--verify-test` | string | derived from the project's test script | The command that runs the tests, instead of the project's own test script. |

Three notes on the set:

- `--to` was removed: SQLite only; the 0.x database move is removed. See [databases](#databases) below.
- `--json` is a shared global flag, so it is not declared on the verb.
- `--scan` wins over `--apply` when both are given.

## Verify the result

- Run the project's own checks the way the migration verified each unit: install, format, typecheck, and tests.
- Run `smthrs doctor`. It reports registry, database, runtime, and provider readiness.
- Run `smthrs ls`. It lists the flows discovered under the project, and the migrated flows should appear under their kept positions, such as `pipelines/ci-fast` for `.smithers/workflows/pipelines/ci-fast.tsx`. See [Flow discovery](/docs/guides/flow-discovery/).
- Execute each migrated module flow through its application host, with its action layers registered. Discovery and a successful CLI plan do not establish that the flow executes: the CLI leaves module flows `pending`. [Module flows](/docs/guides/module-flows/) describes this boundary; [the durable flow tutorial](/docs/tutorials/first-flow/) provides a complete executable host.

## Removed commands and flags

Every 0.x verb and flag that 1.0.0-rc.0 removed fails with exit 1 and a sentence that names the replacement, never with a usage error. The sentence ends with a link to this page, `https://smithers.sh/migration/1.0#<anchor>`. Each heading below is one anchor, spelled exactly the way the link spells it, so you land on the entry your error named.

**Time travel and checkpoints.**

### replay

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs replay`. **1.0 path:** `smthrs runs replay <run>` reads committed execution history without re-executing actions.

### rewind

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs rewind`. **1.0 path:** `smthrs runs rewind <run> --at <sequence> --preview`, then `--yes` to restore an eligible historical frame.

### fork

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs fork`. **1.0 path:** `smthrs runs fork <run> --at <sequence>` creates a resumable child with an isolated workspace.

### timetravel

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs timetravel`. **1.0 path:** `smthrs runs inspect|replay|fork|rewind`; see the [CLI reference](/docs/reference/cli/).

### snapshots

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs snapshots`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### restore

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs restore`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### snapshot-hook

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs snapshot-hook`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### revert

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs revert`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### retry-task

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs retry-task`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### tree

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs tree`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### timeline

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs timeline`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### diff

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs diff`. **1.0 path:** the `@smthrs/time-travel` library; see [Time travel](/docs/concepts/time-travel/).

### worktrees

**Reason:** Use `smthrs runs inspect|replay|fork|rewind` for current execution history. **Removed:** `smthrs worktrees list` and `smthrs worktrees prune`. **1.0 path:** `smthrs runs fork` retains an isolated workspace under `.flows/forks/`; separate worktree list/prune commands are not exposed.

**Hijack and pause.**

### hijack

**Reason:** Not available; use `steer`, `signal`, `approve`, `deny`, `cancel`, `run --resume`. **Removed:** `smthrs hijack`, and the flag `smthrs steer --takeover` (hijack is not available; `steer --message` is the only mode). **1.0 path:** control a live run with `smthrs steer`, `smthrs signal`, `smthrs approve`, `smthrs deny`, or `smthrs cancel`, and resume a parked one with `smthrs run --resume`; see [Steering runs](/docs/guides/steering/).

### pause

**Reason:** Not available; use `steer`, `signal`, `approve`, `deny`, `cancel`, `run --resume`. **Removed:** `smthrs pause`. **1.0 path:** `smthrs cancel` ends a run and `smthrs run --resume` resumes a parked one; see [Steering runs](/docs/guides/steering/).

**Old gateway and UI hosting.**

### gateway

**Reason:** Replaced by `smthrs serve`; the terminal monitor is deleted. **Removed:** `smthrs gateway status` and `smthrs gateway stop`. **1.0 path:** bare `smthrs gateway` survives as an alias of `smthrs serve`, which hosts the control server for the project; see [Control plane](/docs/guides/control-plane/).

### ui

**Reason:** Replaced by `smthrs serve`; the terminal monitor is deleted. **Removed:** `smthrs ui`, and the flags `smthrs up --serve`, `smthrs up --interactive`, `smthrs up --supervise`, `smthrs up --herdr`, `smthrs up --monitor`, and `smthrs up --report`. **1.0 path:** `smthrs serve` hosts the control server; see [Control plane](/docs/guides/control-plane/).

### gui

**Reason:** Replaced by `smthrs serve`; the terminal monitor is deleted. **Removed:** `smthrs gui`. **1.0 path:** `smthrs serve` hosts the control server; see [Control plane](/docs/guides/control-plane/).

### monitor

**Reason:** Replaced by `smthrs serve`; the terminal monitor is deleted. **Removed:** `smthrs monitor`. **1.0 path:** `smthrs serve` hosts the control server; see [Control plane](/docs/guides/control-plane/).

**Supervision.**

### supervise

**Reason:** The run driver's heartbeat sweep owns recovery. **Removed:** `smthrs supervise`. **1.0 path:** recovery is owned by the run driver; watch runs with `smthrs ps` and `smthrs status`; see [Inspecting runs](/docs/guides/inspecting-runs/).

### supervisor

**Reason:** The run driver's heartbeat sweep owns recovery. **Removed:** `smthrs supervisor`. **1.0 path:** recovery is owned by the run driver; watch runs with `smthrs ps` and `smthrs status`; see [Inspecting runs](/docs/guides/inspecting-runs/).

### top

**Reason:** The run driver's heartbeat sweep owns recovery. **Removed:** `smthrs top`. **1.0 path:** recovery is owned by the run driver; watch runs with `smthrs ps` and `smthrs status`; see [Inspecting runs](/docs/guides/inspecting-runs/).

**Evaluation and optimization.**

### optimize

**Reason:** Use `smthrs eval list|run|baseline|compare`; automatic optimization remains unavailable. **Removed:** `smthrs optimize`. **1.0 path:** no replacement ships in rc.0.

### scores

**Reason:** Use `smthrs eval list|run|baseline|compare`; automatic optimization remains unavailable. **Removed:** `smthrs scores`. **1.0 path:** use `smthrs eval run|compare` for fixed-suite scores and baseline regressions.

**Chat and narration.**

### chat

**Reason:** Removed with the JSX inline workflow. **Removed:** `smthrs chat`. **1.0 path:** author the flow as `flows/<name>/flow.mdx` or `flows/<name>/flow.ts` and run it with `smthrs up`; see [Markdown flows](/docs/guides/markdown-flows/) and [Module flows](/docs/guides/module-flows/).

### chat-create

**Reason:** Removed with the JSX inline workflow. **Removed:** `smthrs chat-create`. **1.0 path:** author the flow as `flows/<name>/flow.mdx` or `flows/<name>/flow.ts` and run it with `smthrs up`; see [Markdown flows](/docs/guides/markdown-flows/) and [Module flows](/docs/guides/module-flows/).

### what

**Reason:** Removed with the JSX inline workflow. **Removed:** `smthrs what`. **1.0 path:** author the flow as `flows/<name>/flow.mdx` or `flows/<name>/flow.ts` and run it with `smthrs up`; see [Markdown flows](/docs/guides/markdown-flows/) and [Module flows](/docs/guides/module-flows/).

### ask

**Reason:** Removed with the JSX inline workflow. **Removed:** `smthrs ask`. **1.0 path:** author the flow as `flows/<name>/flow.mdx` or `flows/<name>/flow.ts` and run it with `smthrs up`; see [Markdown flows](/docs/guides/markdown-flows/) and [Module flows](/docs/guides/module-flows/).

**Accounts and providers.**

### agents

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs agents add`, `smthrs agents list`, `smthrs agents reauth`, `smthrs agents remove`, `smthrs agents test`, `smthrs agents capabilities`, and `smthrs agents doctor`. **1.0 path:** seats resolve from provider environment keys, and `smthrs doctor` reports provider readiness; see [Model seats](/docs/guides/model-seats/) and [Environment variables](/docs/reference/environment-variables/).

### usage

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs usage`. **1.0 path:** no replacement ships in rc.0.

### claude-shell

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs claude-shell`. **1.0 path:** no replacement ships in rc.0.

### hermes

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs hermes`. **1.0 path:** no replacement ships in rc.0.

### listeners

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs listeners`. **1.0 path:** no replacement ships in rc.0.

### observability

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs observability`. **1.0 path:** the kernel's telemetry and the OTLP layer; see [Observability](/docs/guides/observability/).

### alerts

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs alerts`. **1.0 path:** no replacement ships in rc.0.

### herdr

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs herdr status`, `smthrs herdr attach`, `smthrs herdr open`, and `smthrs herdr clean`. **1.0 path:** no replacement ships in rc.0.

### openapi

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs openapi list` and `smthrs openapi generate`. **1.0 path:** no replacement ships in rc.0.

### token

**Reason:** Moved to the plugins repository or deferred. **Removed:** `smthrs token issue`, `smthrs token exec`, and `smthrs token revoke`. **1.0 path:** no replacement ships in rc.0.

### cron

**Reason:** Use `smthrs triggers register|list|show|enable|disable|fire|serve`. **Removed:** `smthrs cron start`, `smthrs cron add`, `smthrs cron list`, and `smthrs cron rm`. **1.0 path:** `smthrs triggers register|list|show|enable|disable|fire|serve`; schedules run while `smthrs serve` or `smthrs triggers serve` is active.

**Packs and scaffolding.**

### make-workflow

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs make-workflow`. **1.0 path:** `smthrs init` scaffolds `flows/<name>/flow.mdx`; see [CLI reference](/docs/reference/cli/).

### starters

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs starters`. **1.0 path:** `smthrs init` scaffolds `flows/<name>/flow.mdx`; see [CLI reference](/docs/reference/cli/).

### share

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs share`. **1.0 path:** no replacement ships in rc.0; flows live in the project's own `flows/` directory and move with its version control.

### add

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs add`. **1.0 path:** no replacement ships in rc.0; flows live in the project's own `flows/` directory.

### remove

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs remove`. **1.0 path:** no replacement ships in rc.0; flows live in the project's own `flows/` directory.

### eject

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs eject`. **1.0 path:** no replacement ships in rc.0; a migrated project already owns every file under `flows/`.

### upgrade

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs upgrade`. **1.0 path:** `smthrs migrate`; see Plan and apply above.

### packs

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs packs list` and `smthrs packs update`. **1.0 path:** no replacement ships in rc.0; a 1.0 project keeps flows under `flows/` in version control; see [Project layout](/docs/reference/project-layout/).

### workflow

**Reason:** JSX pack tooling is gone; `smthrs migrate` replaces `upgrade`. **Removed:** `smthrs workflow run`, `smthrs workflow path`, `smthrs workflow create`, `smthrs workflow inspect`, `smthrs workflow skills`, and `smthrs workflow doctor`. **1.0 path:** bare `smthrs workflow` is removed too, and `smthrs workflow list` survives as the `ls` alias. `smthrs ls` lists flows, `smthrs up` runs one, and `smthrs doctor` checks readiness; see [Flow discovery](/docs/guides/flow-discovery/).

**Human requests.**

### human

**Reason:** Approvals park the run; use `ps --status waiting-approval`, `approve`, and `deny`. **Removed:** `smthrs human list` and `smthrs human resolve`. **1.0 path:** list parked approvals with `smthrs ps --status waiting-approval`, then answer them with `smthrs approve` or `smthrs deny`; see [Human approval](/docs/tutorials/human-approval/).

### ask-human

**Reason:** Approvals park the run; use `ps --status waiting-approval`, `approve`, and `deny`. **Removed:** `smthrs ask-human`. **1.0 path:** the run parks at the approval; list parked approvals with `smthrs ps --status waiting-approval`, then answer them with `smthrs approve` or `smthrs deny`; see [Human approval](/docs/tutorials/human-approval/).

**Node detail.**

### node

**Reason:** Use `output`, `logs --json`, and the node-output projection. **Removed:** `smthrs node`. **1.0 path:** `smthrs output` prints one registered node output and `smthrs logs --json` renders the events; see [Inspecting runs](/docs/guides/inspecting-runs/).

### tail

**Reason:** Use `output`, `logs --json`, and the node-output projection. **Removed:** `smthrs tail`. **1.0 path:** `smthrs logs --follow` streams future events and `smthrs output` prints one registered node output; see [Inspecting runs](/docs/guides/inspecting-runs/).

**Review and release.**

### release

**Reason:** Not an rc.0 verb. **Removed:** `smthrs release`. **1.0 path:** no replacement ships in rc.0.

**Old aliases and did-you-mean keys.**

### list-runs

**Reason:** Use `ps`. **Removed:** `smthrs list-runs`. **1.0 path:** `smthrs ps`; see [CLI reference](/docs/reference/cli/).

### list

**Reason:** Use `ls`. **Removed:** `smthrs list`. **1.0 path:** `smthrs ls`; see [CLI reference](/docs/reference/cli/).

### workflows

**Reason:** Use `ls`. **Removed:** `smthrs workflows`. **1.0 path:** `smthrs ls`; see [CLI reference](/docs/reference/cli/).

### stop

**Reason:** Use `cancel`. **Removed:** `smthrs stop`. **1.0 path:** `smthrs cancel`; see [CLI reference](/docs/reference/cli/).

### kill

**Reason:** Use `cancel`. **Removed:** `smthrs kill`. **1.0 path:** `smthrs cancel`; see [CLI reference](/docs/reference/cli/).

### start

**Reason:** Use `up`. **Removed:** `smthrs start`. **1.0 path:** `smthrs up`; see [CLI reference](/docs/reference/cli/).

### exec

**Reason:** Use `up`. **Removed:** `smthrs exec`. **1.0 path:** `smthrs up`; see [CLI reference](/docs/reference/cli/).

### log

**Reason:** Use `logs`. **Removed:** `smthrs log`. **1.0 path:** `smthrs logs`; see [CLI reference](/docs/reference/cli/).

### help

**Reason:** Use `--help`. **Removed:** `smthrs help`. **1.0 path:** `smthrs --help`; see [CLI reference](/docs/reference/cli/).

**Supervision and recovery.**

### supervision

**Reason:** The run driver's heartbeat sweep owns recovery. **Removed:** the flags `smthrs up --force`, `smthrs up --steal-ownership`, `smthrs up --resume-claim-owner`, `smthrs up --resume-claim-heartbeat`, `smthrs up --resume-restore-owner`, and `smthrs up --resume-restore-heartbeat`. **1.0 path:** `smthrs run --resume` resumes a parked run, and the run driver owns takeover and recovery; see [Inspecting runs](/docs/guides/inspecting-runs/).

**Plan admission flags.**

### plan-admission

**Reason:** Parallelism is declared by the flow and bounded by plan admission. **Removed:** the flag `smthrs up --max-concurrency`. **1.0 path:** declare parallelism in the flow and let plan admission bound it; see [Flows, actions, and plans](/docs/concepts/flows-actions-plans/) and [Plan, approve, run](/docs/guides/plan-approve-run/).

**Database and backend flags.**

### databases

**Reason:** SQLite only; the 0.x database move is removed. **Removed:** the flags `smthrs migrate --to` and `--backend` (SQLite only; `--backend sqlite` is accepted as a no-op). **1.0 path:** local SQLite; rc.0 has no PostgreSQL or PGlite backend, and `SMITHERS_BACKEND=pglite` or `SMITHERS_BACKEND=postgres` fails the same way, exiting 1 with `unsupported_database`. See [Unsupported database](/docs/troubleshooting/unsupported-database/).

**Scaffolding flags.**

### init

**Reason:** Rc.0 has no global pack; seats resolve from environment keys. **Removed:** the flag `smthrs init --global`. **1.0 path:** the `smthrs init` verb itself stays; seats resolve from provider environment keys; see [Model seats](/docs/guides/model-seats/) and [Environment variables](/docs/reference/environment-variables/).

**Reserved flow ids.**

### flows

This anchor is not a removal. Verbs that take a flow id refuse the reserved `system/*` ids: a reserved system flow id carries no body in 1.0.0-rc.0, so a launch would park with nothing to run. **1.0 path:** name a flow from `smthrs ls`; see [Flow discovery](/docs/guides/flow-discovery/).

## Runtime state does not carry over

rc.0 never loads, resumes, or migrates a 0.x run database. It reads one in exactly two places, and both reads are read-only: `smthrs doctor` and `smthrs migrate` open `smithers.db` so they can name the runs that have not finished. Nothing resumes those runs, and no tool imports 0.x history. The policy itself is frozen on the [Compatibility policy](/docs/migration/compatibility/) page.

### run-data

Two messages link to this anchor.

The notice written to stderr when a command finds 0.x state beside a project that has no rc.0 state (with the real path in place of `<path>`):

```text
Found Smithers 0.x state at <path>. 1.0.0-rc.0 does not load, resume, or migrate 0.x run databases. Finish, archive, or discard those runs with the 0.x CLI (bunx smthrs@0.35.0 ps), then run "smthrs migrate" to convert the project source.
```

The `smthrs migrate` refusal when a `smithers.db` beside the project still holds runs that have not finished, or cannot be opened:

```text
Refusing to migrate: this project still holds Smithers 0.x run state.
```

The refusal lists each database and, under it, each run with its id, status, and workflow name, then tells you to finish, archive, or discard the runs with the 0.x CLI (`bunx smthrs@0.35.0 ps`) and run the command again. No flag releases this refusal; `--acknowledge-run-state` covers only the run-state gate described in [The two gates](#the-two-gates).

---

# Define and run
https://smithers.sh/docs/examples/01-define-and-run/

Define a typed flow and run it on the in-memory engine.

`Action.make` declares the greeting operation and its schemas. `toLayer` supplies its implementation. The flow body describes one `Greet.call`, which the interpreter executes through the registered implementation.

`FlowEngine.layerMemory` keeps state in the process. The result is useful for learning the authoring API and for tests; it does not survive process exit.

## Run the test

The complete program is [`examples/src/01-define-and-run.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/01-define-and-run.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/01-define-and-run.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/)

## Source

```ts title="examples/src/01-define-and-run.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"

export const Greet = Action.make("examples/Greet", {
  payload: { name: Schema.String },
  success: Schema.String
})

export const Greeting = Flow.make("examples/Greeting", {
  payload: { name: Schema.String },
  success: Schema.String,
  body: (payload) => Greet.call(payload)
})

const GreetingLayer = Layer.mergeAll(
  Greet.toLayer(({ name }) => Effect.succeed(`Hello, ${name}.`)),
  Interpreter.layer(Greeting)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  // An action dispatch is recorded under a derived step identity, so the
  // engine needs a `Crypto` even in memory. A browser program supplies its own;
  // this one is Node.
  Layer.provideMerge(NodeCrypto.layer)
)

/**
 * Runs the flow under a caller-selected execution ID. Without this option or
 * an `idempotencyKey`, the default source mints a fresh UUID for each call.
 * Retain the id to reattach to the same execution.
 */
export const main: Effect.Effect<string> = Greeting.execute(
  { name: "Ada" },
  { executionId: "greeting-ada-1" }
).pipe(
  // `execute` fails typed when a payload does not satisfy the flow's schema.
  // This example constructs a statically valid payload, so a refusal here
  // would be a defect in the example, not an error a caller handles.
  Effect.orDie,
  Effect.provide(GreetingLayer)
)
```

---

# Examples
https://smithers.sh/docs/examples/

The examples live in `examples/src`, grouped below by task. Their pages include the complete source and a command for the dedicated test where one exists. `pnpm run test:examples` runs the example test suite; live-provider tests require credentials and can be skipped. The suite covers real package behavior: the durable examples open real SQLite files, the host example spawns a real process, and the browser example is bundled by a real bundler.

## Basics

| Example | What it shows |
| --- | --- |
| [01 define-and-run](/docs/examples/01-define-and-run/) | Define a typed flow and run it on the in-memory engine. |
| [02 run-durably](/docs/examples/02-run-durably/) | Run a flow on the durable engine and read the journal it wrote. |
| [04 retry-policy](/docs/examples/04-retry-policy/) | Retry a flaky action, and read the policy that decides when to stop. |
| [16 fan-out-fan-in](/docs/examples/16-fan-out-fan-in/) | Fan out over a fixed set of checks two at a time, urgent ones first, then join every verdict into one report. |

## Durability

| Example | What it shows |
| --- | --- |
| [03 crash-and-resume](/docs/examples/03-crash-and-resume/) | Suspend a run, drop the engine, and resume from durable state. |
| [05 time-travel-fork](/docs/examples/05-time-travel-fork/) | Fork a finished run at a journal frame and drive the copy. |
| [06 time-travel-rewind](/docs/examples/06-time-travel-rewind/) | Rewind a run to an earlier frame and re-derive a view. |
| [07 sync-follower](/docs/examples/07-sync-follower/) | Follow a run's journal from a second process. |
| [21 cache-and-compensation](/docs/examples/21-cache-and-compensation/) | Declare how long a recorded result stays good, and what to undo when a step that changed the world fails. |

## Agents

| Example | What it shows |
| --- | --- |
| [11 agent-step](/docs/examples/11-agent-step/) | A model-backed step with a typed output schema, chained into another one. |
| [12 agent-live-smoke](/docs/examples/12-agent-live-smoke/) | The production agent stack against a real model over the network; requires `OPENAI_API_KEY`. |
| [13 agent-live-smoke-local](/docs/examples/13-agent-live-smoke-local/) | The same live smoke against a local Ollama model, with no API key and no external network. |
| [14 agent-live-smoke-gemini](/docs/examples/14-agent-live-smoke-gemini/) | The same live smoke against Gemini's OpenAI-compatible endpoint. |
| [15 model-layer-smoke](/docs/examples/15-model-layer-smoke/) | The model layer itself, live: a route to `Model.stream`, with no cell loop on top. |
| [25 agent-tools-in-sandbox](/docs/examples/25-agent-tools-in-sandbox/) | Give a model-backed step real tools, and run the code it writes inside a sandbox. |
| [26 memory-recall](/docs/examples/26-memory-recall/) | Write facts in one run and recall them in a later run, under one memory policy for the whole flow tree. |
| [39 agent-policies](/docs/examples/39-agent-policies/) | One model-backed step through quota refusal, schema correction, and budget accounting, across a restart. |

## Patterns

| Example | What it shows |
| --- | --- |
| [17 review-loop](/docs/examples/17-review-loop/) | Draft, review, revise, and stop when the reviewer approves or the round budget runs out. |
| [31 bounded-loops](/docs/examples/31-bounded-loops/) | Repair a config until it is clean, then tune a summary; no model, engine, or network. |
| [33 delegation-trellis](/docs/examples/33-delegation-trellis/) | Run a plan a model authored, durably, one trampoline round per plan. |

## Control

| Example | What it shows |
| --- | --- |
| [18 approval-and-signal](/docs/examples/18-approval-and-signal/) | Three gates: a plan approval, an in-run approval, and a signal that ends a durable wait. |
| [19 cancel-and-child-cleanup](/docs/examples/19-cancel-and-child-cleanup/) | Cancel a run, and watch the cancellation reach its linked child and the process its step started. |
| [20 child-flows](/docs/examples/20-child-flows/) | Run two flows as real children of a third, join their results, and see a re-driven parent not run them again. |
| [24 control-plane-and-gateway](/docs/examples/24-control-plane-and-gateway/) | Plan, approve, run, and watch a discovered flow over HTTP and WebSocket on loopback. |
| [30 failure-control](/docs/examples/30-failure-control/) | Bounded checks, a quarantined flake, an escalating fixer, a saga that unwinds a half-finished deploy, and a lock the finalizer always releases. |
| [32 intervene](/docs/examples/32-intervene/) | Read a file, propose an edit, gate the write behind an approval, and report what happened. |
| [34 human-task](/docs/examples/34-human-task/) | Ask a person, refuse the answer, restart the process, and ask again. |
| [34 poll](/docs/examples/34-poll/) | Poll something until it is ready, and survive a restart in the middle of the wait. |
| [36 detached-children](/docs/examples/36-detached-children/) | Spawn a child that outlives its parent, then collect it after a restart. |
| [38 monitor-and-alert](/docs/examples/38-monitor-and-alert/) | Watch a run you do not own, heal it, and page somebody when it stays wrong. |

## Hosts and sandboxes

| Example | What it shows |
| --- | --- |
| [08 host-adapters](/docs/examples/08-host-adapters/) | Run the same host program against two adapters: an in-memory test host, then the real Node host. |
| [09 browser-use](/docs/examples/09-browser-use/) | Use the library from a browser bundle, with no Node built-ins. |
| [22 mcp-server](/docs/examples/22-mcp-server/) | The MCP server the tools example connects to: a separate process speaking JSON-RPC over stdio. |
| [22 mcp-tools](/docs/examples/22-mcp-tools/) | Give a model tools that live in somebody else's process. |
| [35 remote-cache](/docs/examples/35-remote-cache/) | Share step results between two engines through a real HTTP action cache. |
| [37 host-containment](/docs/examples/37-host-containment/) | Kill a host, and watch the next one clean up after it. |
| [40 sandbox-placement](/docs/examples/40-sandbox-placement/) | Keep the durable engine here, and place one action body somewhere else. |
| [41 sandboxed-flow](/docs/examples/41-sandboxed-flow/) | Run a child flow's own code inside a provisioned machine, as one durable action of a parent flow. |

## Operations

| Example | What it shows |
| --- | --- |
| [10 telemetry-export](/docs/examples/10-telemetry-export/) | Add the one OTLP telemetry layer to a durable run and read the same run three ways. |

---

# Run durably
https://smithers.sh/docs/examples/02-run-durably/

Run a flow on the durable engine and read its journal.

The flow calls `Bundle`, whose implementation executes a sealed `Compile` action. The durable engine claims the run, maintains its ownership heartbeat, and persists action attempts. It commits lifecycle events together with the state changes they describe.

## Run the test

The complete program is [`examples/src/02-run-durably.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/02-run-durably.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/02-run-durably.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/)

## Source

```ts title="examples/src/02-run-durably.ts"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/**
 * The declared atom the body names. Declaration and implementation are
 * separate: this value is pure data and travels anywhere, and the layer below
 * attaches the code on a host that can run it.
 */
export const Bundle = Action.make("examples/Bundle", {
  payload: { target: Schema.String },
  success: Schema.String
})

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

/**
 * A sealed action with a cache key input. The identity is what makes the
 * recorded result addressable across runs; without it the action would fall
 * back to a run-local invocation key.
 *
 * DECIDED (2026-08-11, pending review): the target stays OUT of this
 * declaration and is stamped on by the caller. A sealed key is built from the
 * action name, its declared `idempotencyKey`, and its schemas (never from
 * the payload), so moving `target` in here would make two targets share one
 * cache row. Inputs that vary per call belong in the calling step, or in an
 * `idempotencyKey` that names them.
 */
export const Compile = Action.make({
  name: "examples/Compile",
  success: Schema.String,
  tier: "sealed",
  idempotencyKey: "examples/compile/v1",
  execute: Effect.succeed("dist/server.js")
})

export interface Summary {
  readonly result: string
  readonly eventTypes: ReadonlyArray<string>
}

/** Executes the flow, then reads back the durable journal for that run. */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const result = yield* Build.execute({ target: "server" }, { executionId: "build-1" })
    const journal = yield* Journal.Journal
    yield* journal.flush
    const page = yield* journal.entries({ runId: "build-1" as JournalEvent.RunId, limit: 200 })
    return {
      result,
      eventTypes: page.entries.map((entry) => entry.eventType)
    }
  }).pipe(
    Effect.provide(
      Layer.mergeAll(
        Bundle.toLayer(({ target }) => Effect.map(Compile, (artifact) => `${artifact}?target=${target}`)),
        Interpreter.layer(Build)
      ).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(filename, "examples-worker"))
      )
    ),
    Effect.scoped,
    Effect.orDie
  )
```

---

# Crash and resume
https://smithers.sh/docs/examples/03-crash-and-resume/

Suspend a run and resume it through a fresh engine over the same SQLite file.

The first drive waits on a durable deferred and releases ownership. The second drive completes that deferred and finishes the execution.

The counters identify what repeats: the `Assess` implementation enters twice, while the sealed read action before the wait executes once. This simulates an engine restart after a durable suspension; it does not kill a process mid-write.

## Run the test

The complete program is [`examples/src/03-crash-and-resume.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/03-crash-and-resume.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/03-crash-and-resume.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/)

## Source

```ts title="examples/src/03-crash-and-resume.ts"
import { Action, DurableDeferred, Flow, FlowRuntime, Interpreter } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

export const Assess = Action.make("examples/Assess", {
  payload: { document: Schema.String },
  success: Schema.String
})

export const Review = Flow.make("examples/Review", {
  payload: { document: Schema.String },
  success: Schema.String,
  body: (payload) => Assess.call(payload)
})

export const Approval = DurableDeferred.make("examples/approval", {
  success: Schema.String
})

export interface Summary {
  readonly result: string
  readonly readDispatches: number
  readonly stepEntries: number
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let readDispatches = 0
    let stepEntries = 0

    const ReadDocument = Action.make({
      name: "examples/ReadDocument",
      success: Schema.String,
      tier: "sealed",
      idempotencyKey: "examples/read-document/v1",
      execute: Effect.sync(() => {
        readDispatches += 1
        return "draft body"
      })
    })

    const assess = ({ document }: { readonly document: string }) =>
      Effect.gen(function*() {
        stepEntries += 1
        const body = yield* ReadDocument
        const verdict = yield* DurableDeferred.await(Approval)
        return `${document}:${body}:${verdict}`
      })

    const engine = (hostId: string) =>
      Layer.mergeAll(Assess.toLayer(assess), Interpreter.layer(Review)).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(filename, hostId))
      )

    // Phase one: the run suspends at the deferred and releases its claim.
    yield* Effect.scoped(
      Review.execute({ document: "rfc" }, { executionId: "review-1", discard: true }).pipe(
        Effect.provide(engine("worker-a"))
      )
    )

    // Phase two: a fresh engine completes the deferred and finishes the run.
    const result = yield* Effect.scoped(
      Effect.gen(function*() {
        const flowEngine = yield* FlowRuntime.FlowRuntime
        yield* flowEngine.deferredDone(Approval, {
          flowName: Review._tag,
          executionId: "review-1",
          deferredName: Approval.name,
          exit: Exit.succeed("approved")
        })
        return yield* Review.execute({ document: "rfc" }, { executionId: "review-1" })
      }).pipe(Effect.provide(engine("worker-b")))
    )

    return { result, readDispatches, stepEntries }
  }).pipe(Effect.orDie)
```

---

# Retry policy
https://smithers.sh/docs/examples/04-retry-policy/

Inspect a retry policy and retry an action that fails before succeeding.

`RetryPolicy.nextDelay` and `decide` inspect a policy as data. Separately, `Action.retry` wraps an effect with Effect retry options and advances `Action.CurrentAttempt` for durable dispatch. The policy inspection and executed retry loop are separate parts of this example.

## Run the test

The complete program is [`examples/src/04-retry-policy.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/04-retry-policy.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/04-retry-policy.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/)

## Source

```ts title="examples/src/04-retry-policy.ts"
import { Action, Flow, Interpreter, RetryPolicy } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

class Flaky extends Schema.TaggedError<Flaky>()("examples/Flaky", {
  message: Schema.String
}) {}

/** Give up after four attempts; the third is the last that can be retried. */
export const policy = RetryPolicy.make({
  initialMs: 100,
  factor: 2,
  maxMs: 1_000,
  maxAttempts: 4,
  nonRetryable: ["examples/Fatal"]
})

/** The un-jittered backoff ladder this policy produces. */
export const ladder: ReadonlyArray<number | null> = [1, 2, 3, 4].map((attempt) =>
  Option.getOrNull(RetryPolicy.nextDelay(policy, attempt))
)

/** A non-retryable tag short-circuits the ladder regardless of attempt. */
export const fatalDecision = RetryPolicy.decide(policy, {
  attempt: 1,
  error: { _tag: "examples/Fatal" }
})

/**
 * The declared step the flow's body names. Its implementation is where the
 * retry loop lives, because retrying is what the atom does; the plan shows one
 * node either way.
 */
export const Release = Action.make("examples/Release", {
  payload: { release: Schema.String },
  success: Schema.String
})

export const Publish = Flow.make("examples/Publish", {
  payload: { release: Schema.String },
  success: Schema.String,
  body: (payload) => Release.call(payload)
})

export interface Summary {
  readonly result: string
  readonly dispatches: number
  readonly attempts: ReadonlyArray<number>
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let dispatches = 0
    const attempts: Array<number> = []

    const Upload = Action.make({
      name: "examples/Upload",
      success: Schema.String,
      error: Flaky,
      tier: "sealed",
      execute: Effect.gen(function*() {
        const attempt = yield* Action.CurrentAttempt
        attempts.push(attempt)
        dispatches += 1
        return attempt < 3
          ? yield* Effect.fail(new Flaky({ message: `attempt ${attempt} lost the connection` }))
          : "uploaded"
      })
    })

    const publish = ({ release }: { readonly release: string }) =>
      Action.retry(Upload, { times: 3 }).pipe(
        Effect.map((outcome) => `${release}:${outcome}`),
        Effect.catchTag("examples/Flaky", (error) => Effect.succeed(`${release}:failed:${error.message}`))
      )

    const result = yield* Effect.scoped(
      Publish.execute({ release: "v1" }, { executionId: "publish-1" }).pipe(
        Effect.provide(
          Layer.mergeAll(Release.toLayer(publish), Interpreter.layer(Publish)).pipe(
            Layer.provideMerge(Action.layerImplementations),
            Layer.provideMerge(durableEngine(filename, "retry-worker"))
          )
        )
      )
    )

    return { result, dispatches, attempts }
  }).pipe(Effect.orDie)
```

---

# Time travel fork
https://smithers.sh/docs/examples/05-time-travel-fork/

Fork a finished run at a journal frame and drive the child.

`TimeTravel.fork` copies the committed prefix and the attempts it can explain into a child with a separate workspace. The parent remains unchanged. This example forks at the parent's last committed sequence, so the child inherits the complete prefix.

The declared cache environment makes the sealed step key reusable in the child. Without that declaration, the engine scopes the key to its original execution and the fork can execute the body again.

## Run the test

The complete program is [`examples/src/05-time-travel-fork.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/05-time-travel-fork.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/05-time-travel-fork.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/time-travel`](/docs/reference/api/time-travel/)

## Source

```ts title="examples/src/05-time-travel-fork.ts"
import { FlowEngine } from "@smthrs/engine"
import { EngineStore } from "@smthrs/engine-store"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Journal } from "@smthrs/journal"
import { SqlTimeTravelStore, TimeTravel } from "@smthrs/time-travel"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { requirements } from "./durable-layer.ts"

/**
 * The sealed atom the fork replays instead of dispatching again.
 *
 * The declaration is pure data, so it is the same value in the parent's
 * composition and the child's; the implementation is attached per composition
 * below. That split is what lets both engines address one recorded result.
 */
export const Measure = Action.make("examples/Measure", {
  payload: {},
  success: Schema.String,
  tier: "sealed",
  idempotencyKey: "examples/measure/v1"
})

export const Analyse = Flow.make("examples/Analyse", {
  payload: {},
  success: Schema.String,
  body: (payload) => Measure.call(payload)
})

export interface Summary {
  readonly parentResult: string
  readonly forkResult: string
  readonly forkRunId: string
  readonly dispatches: number
  readonly parentEntryCount: number
}

const engineLayer = (filename: string, hostId: string) =>
  EngineStore.layer({
    owner: { hostId },
    journalSource: `${hostId}-engine`
  }).pipe(
    Layer.provideMerge(
      Layer.mergeAll(
        requirements(filename),
        // Declaring the environment is what lets a sealed identity cross the
        // fork boundary.
        Action.layerCacheEnvironment({ layers: [], capabilities: {} })
      )
    )
  )

/**
 * The time-travel service over the same SQLite file the engine writes.
 *
 * `TimeTravel.layer` asks only for injectable contracts (the store, the
 * journal, the run store, the cache, and jj) so it merges straight onto the
 * engine composition. Building it also finishes or rolls back any rewind a
 * crash interrupted, which is why recovery never appears as a call below.
 */
const timeTravelLayer = (filename: string, hostId: string) =>
  TimeTravel.layer.pipe(
    Layer.provideMerge(SqlTimeTravelStore.layer),
    Layer.provideMerge(engineLayer(filename, hostId))
  )

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let dispatches = 0

    const measured = Measure.toLayer(() =>
      Effect.sync(() => {
        dispatches += 1
        return "42"
      })
    )

    /** The implementation and the body that names it, over one table. */
    const analyseLayer = Layer.mergeAll(measured, Interpreter.layer(Analyse)).pipe(
      Layer.provideMerge(Action.layerImplementations)
    )

    const forked = yield* Effect.scoped(
      Effect.gen(function*() {
        const parentResult = yield* Analyse.execute({}, { executionId: "analyse-1" })
        const journal = yield* Journal.Journal
        yield* journal.flush

        const page = yield* journal.entries({ runId: "analyse-1" as never, limit: 200 })
        // The frame to fork at: the last committed sequence for this run.
        const seq = page.entries.at(-1)?.seq ?? 0

        const timeTravel = yield* TimeTravel
        const fork = yield* timeTravel.fork({
          runId: "analyse-1",
          // A frame is addressed by the lineage the ENGINE minted for the run,
          // and `FlowEngine.Lineage` is where that id is minted. It is an opaque
          // versioned encoding, so build it with the constructor rather than
          // spelling one out.
          frame: { lineageId: FlowEngine.Lineage.root("analyse-1"), seq }
        })

        return { parentResult, forkRunId: fork.runId, parentEntryCount: page.entries.length }
      }).pipe(
        Effect.provide(
          analyseLayer.pipe(Layer.provideMerge(timeTravelLayer(filename, "fork-parent")))
        )
      )
    )

    // A fresh engine drives the fork. The copied attempt rows replay.
    const forkResult = yield* Effect.scoped(
      Analyse.execute({}, { executionId: forked.forkRunId }).pipe(
        Effect.provide(analyseLayer.pipe(Layer.provideMerge(engineLayer(filename, "fork-child"))))
      )
    )

    return {
      parentResult: forked.parentResult,
      forkResult,
      forkRunId: forked.forkRunId,
      dispatches,
      parentEntryCount: forked.parentEntryCount
    }
  }).pipe(Effect.orDie)
```

---

# Time travel rewind
https://smithers.sh/docs/examples/06-time-travel-rewind/

Inspect a run at a frame, then rewind its persistent state.

`inspect` folds the committed prefix through a reducer without running actions. `rewind` claims the run, assesses recorded effects, restores the workspace, and archives the suffix after the frame.

The example parks the run on a durable deferred before rewinding, because a live run cannot be taken over for a rewind. Frame lineage metadata comes from engine-written records.

## Run the test

The complete program is [`examples/src/06-time-travel-rewind.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/06-time-travel-rewind.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/06-time-travel-rewind.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/time-travel`](/docs/reference/api/time-travel/)

## Source

```ts title="examples/src/06-time-travel-rewind.ts"
import { FlowEngine } from "@smthrs/engine"
import { Action, DurableDeferred, Flow, Interpreter } from "@smthrs/flow"
import { Journal, JournalEvent } from "@smthrs/journal"
import { SqlTimeTravelStore, TimeTravel } from "@smthrs/time-travel"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as SqlClient from "effect/unstable/sql/SqlClient"
import { durableEngine } from "./durable-layer.ts"

/** The declared step the flow's body names; the wait lives in its implementation. */
export const Post = Action.make("examples/Post", {
  payload: {},
  success: Schema.String
})

export const Ledger = Flow.make("examples/Ledger", {
  payload: {},
  success: Schema.String,
  body: (payload) => Post.call(payload)
})

export const Settlement = DurableDeferred.make("examples/settlement", {
  success: Schema.String
})

/**
 * The run's root lineage, built by the constructor that mints it.
 *
 * A journal lineage id is an opaque versioned encoding, not a path a caller
 * assembles. `FlowEngine.Lineage.root` is the one place it is minted, and the
 * engine stamps the result on every record the run writes, so building the
 * frame's address through it is what makes the frame addressable at all.
 */
const lineageId = FlowEngine.Lineage.root("ledger-1")

/**
 * `TimeTravel.layer` asks only for injectable contracts (the store plus the
 * journal, run, cache, and jj services the engine already provides) so it
 * merges straight onto the engine composition. Building it also resolves any
 * rewind a crash left half-finished, which is why recovery never appears as a
 * call below.
 */
const layer = (filename: string) =>
  Layer.mergeAll(Post.toLayer(post), Interpreter.layer(Ledger)).pipe(
    Layer.provideMerge(Action.layerImplementations),
    Layer.provideMerge(TimeTravel.layer),
    Layer.provideMerge(SqlTimeTravelStore.layer),
    Layer.provideMerge(durableEngine(filename, "ledger"))
  )

export interface Summary {
  readonly derivedAttempts: number
  readonly totalEntries: number
  readonly archivedCount: number
  readonly remainingSeqs: ReadonlyArray<number>
  readonly auditStatus: string
}

const Credit = Action.make({
  name: "examples/Credit",
  success: Schema.Number,
  tier: "sealed",
  idempotencyKey: "examples/credit/v1",
  execute: Effect.succeed(30)
})

const post = () =>
  Effect.gen(function*() {
    const amount = yield* Credit
    const settlement = yield* DurableDeferred.await(Settlement)
    return `${amount}:${settlement}`
  })

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    // Drive the run until it parks at the deferred. It releases ownership on
    // the way out, which is the state a rewind requires.
    yield* Ledger.execute({}, { executionId: "ledger-1", discard: true })

    const journal = yield* Journal.Journal
    yield* journal.flush
    const before = yield* journal.entries({ runId: "ledger-1" as JournalEvent.RunId, limit: 200 })
    // Rewind to the middle of what the run recorded.
    const seq = before.entries[Math.floor(before.entries.length / 2)]!.seq
    const position = { runId: "ledger-1", frame: { lineageId, seq } }

    // Read-only: count the action attempts the frame covers, folding the
    // engine's own records.
    const derivedAttempts = yield* TimeTravel.pipe(
      Effect.flatMap((timeTravel) =>
        timeTravel.inspect(position, {
          initial: 0,
          reduce: (state: number, committed) =>
            committed.eventType === "flows.engine.attempt-started" ? state + 1 : state
        })
      )
    )

    // Mutating: drop everything after the frame.
    const timeTravel = yield* TimeTravel
    const result = yield* timeTravel.rewind(position)

    const remaining = yield* journal.entries({ runId: "ledger-1" as JournalEvent.RunId, limit: 200 })
    const sql = yield* Effect.service(SqlClient.SqlClient)
    const audits = yield* sql<{ readonly status: string }>`
      SELECT status FROM flows_time_travel_audits WHERE id = ${result.auditId}
    `

    return {
      derivedAttempts,
      totalEntries: before.entries.length,
      archivedCount: result.archive.archived,
      remainingSeqs: remaining.entries.map((committed) => committed.seq),
      auditStatus: audits[0]?.status ?? "missing"
    }
  }).pipe(
    Effect.provide(layer(filename)),
    Effect.scoped,
    Effect.orDie
  )
```

---

# Sync follower
https://smithers.sh/docs/examples/07-sync-follower/

Follow a run's journal from a second process's point of view.

The sync protocol is read-only. A follower opens a subscription against a scope and receives the durable history first, then live entries as they commit. Cursors are per run and tolerate sequence holes: `afterSeq` means "entries after this number", never "the next adjacent number".

The wiring below is the same one the package's own suites use: a real `SyncServer` over a real SQL journal, connected to a real `SyncClient` through the in-memory socket pair from `@smthrs/sync/test/TestSocket`. Swap the socket for a network transport and the follower code is unchanged.

## Run the test

The complete program is [`examples/src/07-sync-follower.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/07-sync-follower.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/07-sync-follower.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/sync`](/docs/reference/api/sync/)

## Source

```ts title="examples/src/07-sync-follower.ts"
import { Journal, JournalEvent } from "@smthrs/journal"
import * as TestSocket from "@smthrs/sync/test/TestSocket"
import * as TestSync from "@smthrs/sync/test/TestSync"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import * as Stream from "effect/Stream"

export interface Summary {
  readonly caughtUp: ReadonlyArray<string>
  readonly followed: ReadonlyArray<string>
}

const runId = "sync-demo-1" as JournalEvent.RunId
const sourceId = "examples" as JournalEvent.SourceId

const entry = (sourceSeq: number, eventType: string) =>
  new JournalEvent.Input({
    runId,
    sourceId,
    sourceSeq: sourceSeq as JournalEvent.SourceSeq,
    eventType,
    payload: { sourceSeq },
    meta: null
  })

export const main: Effect.Effect<Summary> = Effect.gen(function*() {
  const journal = yield* Journal.Journal

  // History the follower has never seen.
  yield* journal.emitDurableUnfenced(entry(0, "run.started"))
  yield* journal.emitDurableUnfenced(entry(1, "step.recorded"))

  const pair = yield* TestSocket.makePair()
  const follower = yield* TestSync.connect(pair)

  const collected = yield* Stream.runCollect(
    follower.subscribe({ scope: { _tag: "Run", runId }, cursors: [] }).pipe(Stream.take(3))
  ).pipe(Effect.forkChild({ startImmediately: true }))

  // A live entry committed after the subscription opened.
  yield* journal.emitDurableUnfenced(entry(2, "run.completed"))

  const entries = Array.from(yield* Fiber.join(collected))
  return {
    caughtUp: entries.slice(0, 2).map((committed) => committed.eventType),
    followed: entries.slice(2).map((committed) => committed.eventType)
  }
}).pipe(Effect.provide(TestSync.layerTest), Effect.scoped, Effect.orDie)
```

---

# Host adapters
https://smithers.sh/docs/examples/08-host-adapters/

Run the same host program against two adapters.

The host surface is a closed set of service tags: `FileSystem`, `Path`, `ChildProcessSpawner`, `Jj`, and `HttpClient`. A program written against the tags runs on any bundle that provides them. Note that `ChildProcessSpawner` and `HttpClient` are Effect's own tags from `effect/unstable/process` and `effect/unstable/http`. Smithers provides implementations of them rather than a wrapper around them.

This example runs one program twice:

`TestHost.layer` supplies an in-memory filesystem and a scripted in-process bash interpreter, so the assertions are deterministic and nothing touches the machine.

`NodeHost.layer` supplies the real Node adapters, so the same command spawns a real process.

Browser and Bun bundles implement the same tags and live at `@smthrs/platform-browser`'s `BrowserHost` and `@smthrs/platform-bun`'s `BunHost`.

## Run the test

The complete program is [`examples/src/08-host-adapters.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/08-host-adapters.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/08-host-adapters.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/testing`](/docs/reference/api/testing/)

## Source

```ts title="examples/src/08-host-adapters.ts"
import * as TestHost from "@smthrs/testing/TestHost"
import * as NodeHost from "@smthrs/platform-node/NodeHost"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"

export interface Summary {
  readonly scriptedRead: string
  readonly scriptedExec: string
  readonly nodeExec: string
}

/** The adapter-neutral program: read a file, then run a command. */
const program = Effect.gen(function*() {
  const fs = yield* FileSystem.FileSystem
  const spawner = yield* ChildProcessSpawner
  const bytes = yield* fs.readFile("/workspace/input.txt")
  const executed = yield* spawner.string(ChildProcess.make("read-input"))
  return { read: new TextDecoder().decode(bytes), executed: executed.trim() }
})

export const main: Effect.Effect<Summary> = Effect.gen(function*() {
  const scripted = yield* program.pipe(
    Effect.provide(
      TestHost.layer({
        files: { "/workspace/input.txt": "hello from memory" },
        commands: { "read-input": { stdout: "hello from script\n", exitCode: 0 } }
      })
    )
  )

  const node = yield* Effect.gen(function*() {
    const spawner = yield* ChildProcessSpawner
    return yield* spawner.string(ChildProcess.make("printf", ["hello from node"]))
  }).pipe(Effect.provide(NodeHost.layer))

  return {
    scriptedRead: scripted.read,
    scriptedExec: scripted.executed,
    nodeExec: node
  }
}).pipe(Effect.orDie)
```

---

# Browser use
https://smithers.sh/docs/examples/09-browser-use/

Use the library from a browser bundle.

Every import in this file comes from an entry point that the repository's browser gate (`pnpm run browser`) bundles with esbuild `platform: "browser"`: the `@smthrs/engine`, `@smthrs/keys`, `@smthrs/journal`, and `@smthrs/platform-browser` roots never statically resolve a `node:` built-in, because platform access lives in layers, and the Node and Bun bundles are their own packages.

The barrel `@smthrs/flows` and `@smthrs/engine-store` bundle for the browser as well, and this file still imports the per-package roots: the barrel pulls every engine package into the bundle, and the Node-only composition is the `@smthrs/flows/NodeRuntime` subpath, which opens SQLite through `node:sqlite`. The test for this example bundles the file itself for the browser and fails if a Node-only import ever sneaks in.

## Run the test

The complete program is [`examples/src/09-browser-use.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/09-browser-use.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/09-browser-use.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/keys`](/docs/reference/api/keys/)

## Source

```ts title="examples/src/09-browser-use.ts"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { deriveKey } from "@smthrs/keys"
import type * as Crypto from "effect/Crypto"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"

/**
 * The declared atom. A declaration is pure data, so it bundles for the browser
 * whether or not this bundle also carries an implementation for it, which is
 * the placement half of the declaration/implementation split.
 */
export const CompileTarget = Action.make("examples/CompileTarget", {
  payload: { target: Schema.String },
  success: Schema.String
})

export const Compile = Flow.make("examples/Compile", {
  payload: { target: Schema.String },
  success: Schema.String,
  idempotencyKey: ({ target }) => target,
  body: (payload) => CompileTarget.call(payload)
})

const CompileLayer = Layer.mergeAll(
  CompileTarget.toLayer(({ target }) => Effect.succeed(`built ${target}`)),
  Interpreter.layer(Compile)
).pipe(
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory)
)

export interface Summary {
  readonly result: string
  readonly stepKey: string
}

export const main: Effect.Effect<Summary, never, Crypto.Crypto> = Effect.gen(function*() {
  const result = yield* Compile.execute({ target: "web" })

  const key = yield* deriveKey({
    body: "examples/compile/v1",
    target: "web"
  })

  return { result, stepKey: key }
}).pipe(Effect.provide(CompileLayer), Effect.orDie)
```

---

# Telemetry export
https://smithers.sh/docs/examples/10-telemetry-export/

Add the one telemetry layer to a durable run and read the granular trace data back three ways.

The flow and engine composition are example 02's: nothing in the flow body or the layers changes for telemetry. The store packages already open spans through Effect's tracer and update `Metric` counters on their hot paths; what they deliberately do not do is export. `Otlp.layerFetch` is that exporter (one layer against an OTLP collector endpoint, no OpenTelemetry SDK involved), and providing it is the entire wiring. Deleting the `Effect.provide(telemetry)` line removes telemetry and changes nothing else.

The three read paths this example demonstrates:

1. The OTLP export: every span the packages open (the flow lifecycle, the    engine dispatch, run claims and heartbeats, journal writes, down to    individual `sql.execute` statements) posts to `/v1/traces`, and every    metric series to `/v1/metrics`. 2. The durable journal: `Journal.entries` reads the run's lifecycle events    in-process, no collector involved. 3. The metric handles: `Metric.value` reads a counter in-process, no    exporter involved. An outcome-dimensioned counter is read through its    exported attribute view (`EngineStoreMetrics.dispatch.Success` here):    the engine updates only the tagged series, so the bare counter handle    would read the attribute-less series and always see zero.

`main` wraps the caller's `fetch` with a recorder so the summary can list what the collector received; a production program provides `Otlp.layerFetch` exactly as written here and skips the recording.

## Run the test

The complete program is [`examples/src/10-telemetry-export.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/10-telemetry-export.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/10-telemetry-export.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/observability`](/docs/reference/api/observability/)

## Source

```ts title="examples/src/10-telemetry-export.ts"
import { EngineStoreMetrics } from "@smthrs/engine-store"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as Otlp from "@smthrs/observability/Otlp"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Metric from "effect/Metric"
import * as Schema from "effect/Schema"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
import { durableEngine } from "./durable-layer.ts"

export const Pack = Action.make("examples/Pack", {
  payload: { target: Schema.String },
  success: Schema.String
})

export const Ship = Flow.make("examples/Ship", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload) => Pack.call(payload)
})

export interface Summary {
  readonly result: string
  /** Distinct span names the collector received, sorted. */
  readonly spanNames: ReadonlyArray<string>
  /** Distinct metric series names the collector received, sorted. */
  readonly metricNames: ReadonlyArray<string>
  /** The run's journal event types, in order, read without the collector. */
  readonly journalEventTypes: ReadonlyArray<string>
  /** `Metric.value` on the tagged dispatch view, read without the exporter. */
  readonly successfulDispatches: number
}

/** The narrow slice of an OTLP JSON body the summary reads. */
interface TraceExport {
  readonly resourceSpans?: ReadonlyArray<{
    readonly scopeSpans?: ReadonlyArray<{
      readonly spans?: ReadonlyArray<{ readonly name: string }>
    }>
  }>
}

interface MetricExport {
  readonly resourceMetrics?: ReadonlyArray<{
    readonly scopeMetrics?: ReadonlyArray<{
      readonly metrics?: ReadonlyArray<{ readonly name: string }>
    }>
  }>
}

interface Recorded {
  readonly url: string
  readonly body: string
}

const requestBody = (init: RequestInit | undefined): string =>
  typeof init?.body === "string"
    ? init.body
    : new TextDecoder().decode(init?.body as Uint8Array)

const distinctSorted = (names: ReadonlyArray<string>): ReadonlyArray<string> => [...new Set(names)].sort()

const spanNames = (recorded: ReadonlyArray<Recorded>): ReadonlyArray<string> =>
  distinctSorted(
    recorded
      .filter((request) => request.url.endsWith("/v1/traces"))
      .flatMap((request) => (JSON.parse(request.body) as TraceExport).resourceSpans ?? [])
      .flatMap((resource) => resource.scopeSpans ?? [])
      .flatMap((scope) => scope.spans ?? [])
      .map((span) => span.name)
  )

const metricNames = (recorded: ReadonlyArray<Recorded>): ReadonlyArray<string> =>
  distinctSorted(
    recorded
      .filter((request) => request.url.endsWith("/v1/metrics"))
      .flatMap((request) => (JSON.parse(request.body) as MetricExport).resourceMetrics ?? [])
      .flatMap((resource) => resource.scopeMetrics ?? [])
      .flatMap((scope) => scope.metrics ?? [])
      .map((metric) => metric.name)
  )

/**
 * Executes the flow with telemetry provided, then summarizes what exported.
 *
 * The default `fetchImpl` posts to a local collector; the test passes a stub
 * so the suite needs no network. Export flushes when the telemetry layer's
 * scope closes, so the summary is built after `Effect.scoped` completes.
 */
export const main = (
  filename: string,
  fetchImpl: typeof globalThis.fetch = globalThis.fetch
): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const recorded: Array<Recorded> = []
    const recordingFetch: typeof globalThis.fetch = (input, init) => {
      recorded.push({ url: String(input), body: requestBody(init ?? undefined) })
      return fetchImpl(input, init)
    }

    // The entire telemetry wiring: one layer against a collector endpoint.
    const telemetry = Otlp.layerFetch({
      baseUrl: "http://localhost:4318",
      serviceName: "examples-telemetry"
    })

    const program = Effect.gen(function*() {
      const result = yield* Ship.execute({ target: "server" }, { executionId: "telemetry-1" })

      // Read path 2: the durable journal, no collector involved.
      const journal = yield* Journal.Journal
      yield* journal.flush
      const page = yield* journal.entries({
        runId: "telemetry-1" as JournalEvent.RunId,
        limit: 200
      })

      // Read path 3: the tagged metric view, no exporter involved.
      const dispatches = yield* Metric.value(EngineStoreMetrics.dispatch.Success)

      return {
        result,
        journalEventTypes: page.entries.map((entry) => entry.eventType),
        successfulDispatches: dispatches.count
      }
    })

    const outcome = yield* program.pipe(
      Effect.provide(
        Layer.mergeAll(
          Pack.toLayer(({ target }) => Effect.succeed(`dist/${target}.tar`)),
          Interpreter.layer(Ship)
        ).pipe(
          Layer.provideMerge(Action.layerImplementations),
          Layer.provideMerge(durableEngine(filename, "examples-telemetry"))
        )
      ),
      Effect.provide(telemetry),
      Effect.provideService(FetchHttpClient.Fetch, recordingFetch),
      Effect.provideService(Metric.MetricRegistry, new Map()),
      Effect.scoped
    )

    // The telemetry scope closed above, so the shutdown flush has posted.
    return {
      ...outcome,
      spanNames: spanNames(recorded),
      metricNames: metricNames(recorded)
    }
  }).pipe(Effect.orDie)
```

---

# Agent step
https://smithers.sh/docs/examples/11-agent-step/

Pass typed output from a research action into a writing action.

Both steps use `AgentAction.make`. The research step returns a summary and key points; the writing step consumes those fields. The output schema is included in the agent's instructions and validates its final answer, with a bounded correction attempt for invalid output.

`AgentAction.Host` supplies the callable registry and sandbox budget. `SeatResolver` supplies a scripted model, so this example needs no provider credential.

## Run the test

The complete program is [`examples/src/11-agent-step.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/11-agent-step.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/11-agent-step.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/11-agent-step.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import { Node } from "@smthrs/plan"
import * as Registry from "@smthrs/registry/Registry"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"

const ResearchResult = Schema.Struct({
  summary: Schema.String,
  keyPoints: Schema.Array(Schema.String)
})

const ArticleResult = Schema.Struct({
  article: Schema.String,
  wordCount: Schema.Number
})

/** The research step: a model call whose answer must be a `ResearchResult`. */
export const Research = AgentAction.make("examples/Research", {
  payload: { topic: Schema.String },
  output: ResearchResult,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You are a research assistant. Provide concise, accurate summaries."],
  prompt: ({ topic }) => `Research the topic "${topic}" and report what matters about it.`
})

/** The writing step, which consumes the first step's typed fields. */
export const Write = AgentAction.make("examples/Write", {
  payload: {
    summary: Schema.String,
    keyPoints: Schema.Array(Schema.String)
  },
  output: ArticleResult,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You are a technical writer. Write clear, engaging content."],
  prompt: ({ keyPoints, summary }) =>
    `Write a short article from this research.\n\nSummary: ${summary}\n\nKey points:\n${
      keyPoints.map((point) => `- ${point}`).join("\n")
    }`
})

/**
 * The workflow. Nothing executes at plan time: `Research.call` records one
 * node, and `Node.bindPlanned` feeds its planned result into `Write.call`.
 */
export const SimpleWorkflow = Flow.make("examples/SimpleWorkflow", {
  payload: { topic: Schema.String },
  success: ArticleResult,
  error: AgentAction.AgentFailure,
  body: ({ topic }) =>
    Research.call({ topic }).pipe(
      Node.bindPlanned((research) => Write.call({ summary: research.summary, keyPoints: research.keyPoints }))
    )
})

const prepared: Route.PreparedRequest = {
  routeId: "examples",
  protocolId: "examples",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

/**
 * A scripted model: it answers each call with one fenced `cell` block.
 *
 * A cell does not return its transition. It runs in a REPL realm that outlives
 * the frame and states its intent by calling: `ctx.done(output)` completes the
 * run, `ctx.park(reason, message)` waits durably, and a cell that calls neither
 * gets another frame. The host renders a structured `output` as canonical JSON,
 * which is the text the declared schema decodes. A real seat streams the same
 * events from a provider.
 */
const scripted: Model.Model = Model.make({
  stream: (request) =>
    Stream.suspend(() => {
      // The loop keeps the task in a stable system prefix and rebuilds the
      // message tail every frame, so both halves of the request have to be read
      // to see what was asked, which is what a real model does too.
      const asked = [
        ...request.system.map((part) => part.text),
        ...request.messages.flatMap((message) =>
          message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
        )
      ].join("\n")
      const answer = asked.includes("Write a short article")
        ? {
          article: "Durable workflows survive restarts because their steps are recorded, not remembered.",
          wordCount: 12
        }
        : {
          summary: "Durable workflows record every step so a restart resumes instead of repeating.",
          keyPoints: ["steps are journaled", "replay is deterministic"]
        }
      const cell = `ctx.done(${JSON.stringify(answer)})`
      return Stream.fromIterable([
        ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
        ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
        ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
        ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
      ])
    })
})

/**
 * The host: one registry and one sandbox budget, shared by every model-backed
 * action in the composition.
 */
const host = AgentAction.layerHost({
  registry: Registry.makeNoop({
    list: () => Effect.succeed([]),
    visible: () => Effect.succeed([]),
    getOption: () => Effect.succeed(Option.none())
  }),
  limits: { calls: 8 },
  capabilityEnvelope: [],
  maxFrames: 4
})

/**
 * The credentialed half, and the only seam between this deterministic run and a
 * live one: every declared seat resolves to the scripted model above. A real
 * host answers with a provider route and a real context window instead.
 */
const seats = SeatResolver.layer({
  resolve: (id) =>
    Effect.succeed(
      Seat.make({
        id,
        model: scripted,
        route: { prepare: () => Effect.succeed(prepared) },
        contextWindowTokens: 200_000
      })
    )
})

const SimpleWorkflowLayer = Layer.mergeAll(
  Research.layer,
  Write.layer,
  Interpreter.layer(SimpleWorkflow)
).pipe(
  // The agent itself is the production loop; only the host services around it
  // are scripted.
  Layer.provideMerge(Layer.mergeAll(host, seats, Agent.layer)),
  // Pointing the resolver at a provider should park reset-bearing refusals.
  // This standalone flow has no approved envelope from which to derive a cap.
  // eslint-disable-next-line no-restricted-syntax -- this standalone flow has no approved envelope
  Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerDefault(), Budget.layerUnbounded())),
  // The sandbox a cell's code runs in and the steering source it drains. Both
  // are browser-safe defaults; a host that accepts mid-run messages provides
  // its own `Steering.layer` instead.
  Layer.provideMerge(Agent.layerDefaults),
  Layer.provideMerge(Action.layerImplementations),
  Layer.provideMerge(FlowEngine.layerMemory),
  Layer.provideMerge(NodeCrypto.layer)
)

/**
 * Runs the workflow. The result is `ArticleResult`-typed all the way out: no
 * caller parses model text. `orDie` runs after the layer is provided, so a
 * sandbox that fails to build is a defect here too rather than an error the
 * caller has to handle.
 */
export const main: Effect.Effect<typeof ArticleResult.Type> = SimpleWorkflow.execute(
  { topic: "durable workflows" },
  { executionId: "agent-step-1" }
).pipe(
  Effect.provide(SimpleWorkflowLayer),
  Effect.orDie
)
```

---

# Agent live smoke
https://smithers.sh/docs/examples/12-agent-live-smoke/

Run a model-backed action against a live OpenAI provider.

This exercises the agent loop, action adapter, and engine with a network request. `main` reads `OPENAI_API_KEY`; importing the module does not call the provider. The dedicated test requires both the key and `SMITHERS_LIVE_EXAMPLES=1`; a skipped result does not verify live execution. The ordinary example suite clears the key in its test environment. From `examples/`, run `SMITHERS_LIVE_EXAMPLES=1 pnpm exec vitest run test/12-agent-live-smoke.test.ts`. The test allows 300 seconds for the model round trip.

## Run the test

The complete program is [`examples/src/12-agent-live-smoke.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/12-agent-live-smoke.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/12-agent-live-smoke.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/12-agent-live-smoke.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as FlowEngineLike from "@smthrs/agent/FlowEngineLike"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as GrantStore from "@smthrs/kernel/GrantStore"
import * as KernelHttpClient from "@smthrs/kernel/HttpClient"
import * as RequestExecutor from "@smthrs/model/RequestExecutor"
import * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import { Effect, Layer, Option, Redacted, Schema } from "effect"

/**
 * The real HTTP transport: the kernel's guarded client (always-allow, since
 * this is a standalone smoke test with no grant policy to enforce) over
 * Node's real undici-backed client.
 *
 * @category layers
 * @since 0.1.0
 */
export const executorLayer = RequestExecutor.layer.pipe(
  Layer.provide(KernelHttpClient.layer),
  Layer.provide(GrantStore.layerNoop),
  Layer.provide(NodeHttpClient.layerUndici)
)

/**
 * A real `SeatResolver`: any `openai:<modelId>` seat resolves to a live
 * OpenAI Chat Completions route, credentialed from `OPENAI_API_KEY`. Mirrors
 * `packages/smithers/src/NodeControl.ts`'s `seatOf`, narrowed to one provider.
 *
 * @category layers
 * @since 0.1.0
 */
export const liveOpenAiSeats = (apiKey: string) =>
  Layer.effect(SeatResolver.SeatResolver)(
    Effect.gen(function*() {
      const executor = yield* RequestExecutor.RequestExecutor
      return SeatResolver.make({
        resolve: (id) =>
          Effect.gen(function*() {
            const modelId = Seat.modelIdOf(id)
            const routeConfig = yield* Effect.fromResult(Route.openai({ apiKey: Redacted.make(apiKey) })).pipe(
              Effect.mapError((error) => new Seat.SeatUnresolved({ seat: id, message: error.message }))
            )
            const model = yield* Route.toModel(routeConfig).pipe(
              Effect.provideService(RequestExecutor.RequestExecutor, executor)
            )
            return Seat.make({
              id,
              model,
              route: FlowEngineLike.routeResolver(routeConfig),
              contextWindowTokens: SeatResolver.contextWindowTokensFor(modelId)
            })
          })
      })
    })
  ).pipe(Layer.provide(executorLayer))

/** One model-backed step: answer a question in one short sentence. */
export const LiveSmoke = AgentAction.make("examples/LiveSmoke", {
  payload: { question: Schema.String },
  output: Schema.Struct({ answer: Schema.String }),
  seat: "openai:gpt-4o-mini",
  system: ["You are a terse assistant. Answer in one short sentence and nothing else."],
  prompt: ({ question }) => question
})

/** The workflow: one step, so the live network round trip is the only thing under test. */
export const LiveSmokeWorkflow = Flow.make("examples/LiveSmokeWorkflow", {
  payload: { question: Schema.String },
  success: Schema.Struct({ answer: Schema.String }),
  error: AgentAction.AgentFailure,
  body: ({ question }) => LiveSmoke.call({ question })
})

const host = AgentAction.layerHost({
  registry: Registry.makeNoop({
    list: () => Effect.succeed([]),
    visible: () => Effect.succeed([]),
    getOption: () => Effect.succeed(Option.none())
  }),
  limits: { calls: 4 },
  capabilityEnvelope: [],
  maxFrames: 3
})

/**
 * Builds the full live composition. Takes the API key explicitly rather than
 * reading `process.env` at module load, so importing this module never
 * throws in an environment with no key configured. Only running `main`
 * does.
 *
 * @category layers
 * @since 0.1.0
 */
export const liveLayer = (apiKey: string) =>
  Layer.mergeAll(LiveSmoke.layer, Interpreter.layer(LiveSmokeWorkflow)).pipe(
    Layer.provideMerge(Layer.mergeAll(host, liveOpenAiSeats(apiKey), Agent.layer)),
    // Live refusals should park when the provider gives a reset. This smoke
    // run has no approved plan envelope from which to derive a spend ceiling.
    // eslint-disable-next-line no-restricted-syntax -- this standalone smoke has no approved envelope
    Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerDefault(), Budget.layerUnbounded())),
    Layer.provideMerge(Agent.layerDefaults),
    Layer.provideMerge(Action.layerImplementations),
    Layer.provideMerge(FlowEngine.layerMemory),
    Layer.provideMerge(NodeCrypto.layer)
  )

/**
 * Runs the workflow against a real OpenAI seat. Fails at construction with a
 * plain `Error` when `OPENAI_API_KEY` is unset, so a caller sees an honest
 * refusal instead of a mysterious `SeatUnresolved` three layers down.
 *
 * @category constructors
 * @since 0.1.0
 */
export const main = (question: string) => {
  const apiKey = process.env.OPENAI_API_KEY
  if (apiKey === undefined || apiKey === "") {
    throw new Error("Set OPENAI_API_KEY to run this live smoke test")
  }
  return LiveSmokeWorkflow.execute(
    { question },
    { executionId: `live-smoke-${Date.now()}` }
  ).pipe(
    Effect.orDie,
    Effect.provide(liveLayer(apiKey))
  )
}
```

---

# Agent live smoke local
https://smithers.sh/docs/examples/13-agent-live-smoke-local/

Run a model-backed action against a local Ollama endpoint.

The composition uses `Route.openaiChatCompatible` with a local model server. Start Ollama and download the model configured by the example before running it. `main` accepts the server base URL and defaults to `http://localhost:11434`.

This example needs a running local model; it is not a deterministic substitute for the scripted-model tests.

The test requires explicit opt-in even when Ollama is running. From `examples/`, run `SMITHERS_LIVE_EXAMPLES=1 pnpm exec vitest run test/13-agent-live-smoke-local.test.ts`. It allows 300 seconds and skips with a reason if the daemon or model is missing.

## Run the test

The complete program is [`examples/src/13-agent-live-smoke-local.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/13-agent-live-smoke-local.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/13-agent-live-smoke-local.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/13-agent-live-smoke-local.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as FlowEngineLike from "@smthrs/agent/FlowEngineLike"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as GrantStore from "@smthrs/kernel/GrantStore"
import * as KernelHttpClient from "@smthrs/kernel/HttpClient"
import * as ModelRequest from "@smthrs/model/ModelRequest"
import * as RequestExecutor from "@smthrs/model/RequestExecutor"
import * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import { Effect, Layer, Option, Redacted, Schema } from "effect"

/**
 * The real HTTP transport: the kernel's guarded client (always-allow, since
 * this is a standalone smoke test with no grant policy to enforce) over
 * Node's real undici-backed client.
 *
 * @category layers
 * @since 0.1.0
 */
export const executorLayer = RequestExecutor.layer.pipe(
  Layer.provide(KernelHttpClient.layer),
  Layer.provide(GrantStore.layerNoop),
  Layer.provide(NodeHttpClient.layerUndici)
)

/**
 * A real `SeatResolver`: any `local:<modelId>` seat resolves to a live Ollama
 * route over its OpenAI-compatible Chat Completions endpoint. No credential
 * is required (Ollama ignores its `Authorization` header), so a fixed
 * non-empty placeholder satisfies `Auth.bearer`'s only invariant.
 *
 * @category layers
 * @since 0.1.0
 */
export const liveLocalSeats = (baseUrl: string) =>
  Layer.effect(SeatResolver.SeatResolver)(
    Effect.gen(function*() {
      const executor = yield* RequestExecutor.RequestExecutor
      return SeatResolver.make({
        resolve: (id) =>
          Effect.gen(function*() {
            const modelId = Seat.modelIdOf(id)
            const routeConfig = yield* Effect.fromResult(
              Route.openaiChatCompatible({ id: "local", baseUrl, apiKey: Redacted.make("local") })
            ).pipe(
              Effect.mapError((error) => new Seat.SeatUnresolved({ seat: id, message: error.message }))
            )
            const model = yield* Route.toModel(routeConfig).pipe(
              Effect.provideService(RequestExecutor.RequestExecutor, executor)
            )
            return Seat.make({
              id,
              model,
              route: FlowEngineLike.routeResolver(routeConfig),
              contextWindowTokens: SeatResolver.contextWindowTokensFor(modelId)
            })
          })
      })
    })
  ).pipe(Layer.provide(executorLayer))

/**
 * One model-backed step: answer a question in one short sentence.
 *
 * Three declarations make a 7B model finish this step the same way every run.
 *
 * The system teaching spells out the call the cell runtime is waiting for. A
 * model told only to "answer in one short sentence" finishes with
 * `ctx.done("Paris")`, which is a fine sentence and not a document, and the
 * run then failed with `"Paris" is not valid JSON`.
 *
 * `corrections: 3` re-prompts a near miss instead of failing the step on the
 * first one; the default is a single correction.
 *
 * `temperature: 0` pins the seat to greedy decoding. Sampling was the whole
 * remaining flake: at the provider default the same prompt sometimes spent
 * all eight frames writing prose and never called `ctx.done`, and the step
 * failed with `model_failed`, "ended without a completed answer": the
 * last flake observed by the examples gate. Greedy decoding
 * answered 20 of 20 direct runs and 12 of 12 suite runs.
 */
export const LiveSmokeLocal = AgentAction.make("examples/LiveSmokeLocal", {
  payload: { question: Schema.String },
  output: Schema.Struct({ answer: Schema.String }),
  seat: "local:qwen2.5:7b",
  system: [
    "You are a terse assistant. Answer in one short sentence.",
    "Your answer goes inside a JSON object, never on its own.",
    "Finish with exactly this call, with your answer in place of the placeholder:",
    "await ctx.done(JSON.stringify({ answer: \"<your one-sentence answer>\" }))"
  ],
  corrections: 3,
  modelParams: ModelRequest.GenerationParams.make({ temperature: 0 }),
  prompt: ({ question }) => question
})

/** The workflow: one step, so the live round trip to the local model is the only thing under test. */
export const LiveSmokeLocalWorkflow = Flow.make("examples/LiveSmokeLocalWorkflow", {
  payload: { question: Schema.String },
  success: Schema.Struct({ answer: Schema.String }),
  error: AgentAction.AgentFailure,
  body: ({ question }) => LiveSmokeLocal.call({ question })
})

const host = AgentAction.layerHost({
  registry: Registry.makeNoop({
    list: () => Effect.succeed([]),
    visible: () => Effect.succeed([]),
    getOption: () => Effect.succeed(Option.none())
  }),
  limits: { calls: 10 },
  capabilityEnvelope: [],
  maxFrames: 8
})

/**
 * Builds the full live composition against a local Ollama endpoint. Takes
 * `baseUrl` explicitly rather than hardcoding it, so a caller can point this
 * at any Chat-Completions-compatible local server.
 *
 * @category layers
 * @since 0.1.0
 */
export const liveLayer = (baseUrl: string) =>
  Layer.mergeAll(LiveSmokeLocal.layer, Interpreter.layer(LiveSmokeLocalWorkflow)).pipe(
    Layer.provideMerge(Layer.mergeAll(host, liveLocalSeats(baseUrl), Agent.layer)),
    // A local provider can still name a reset, so honor it. This standalone
    // smoke run has no approved plan envelope from which to derive a ceiling.
    // eslint-disable-next-line no-restricted-syntax -- this standalone smoke has no approved envelope
    Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerDefault(), Budget.layerUnbounded())),
    Layer.provideMerge(Agent.layerDefaults),
    Layer.provideMerge(Action.layerImplementations),
    Layer.provideMerge(FlowEngine.layerMemory),
    Layer.provideMerge(NodeCrypto.layer)
  )

/**
 * Runs the workflow against a local Ollama seat.
 *
 * @category constructors
 * @since 0.1.0
 */
export const main = (question: string, baseUrl = "http://localhost:11434") =>
  LiveSmokeLocalWorkflow.execute(
    { question },
    { executionId: `live-smoke-local-${Date.now()}` }
  ).pipe(
    Effect.orDie,
    Effect.provide(liveLayer(baseUrl))
  )

if (import.meta.url === `file://${process.argv[1]}`) {
  Effect.runPromise(main("What is the capital of France? Answer in one word.")).then(
    (result) => {
      console.log("RESULT:", JSON.stringify(result))
    },
    (error) => {
      console.error("FAILED:", error)
      process.exitCode = 1
    }
  )
}
```

---

# Agent live smoke gemini
https://smithers.sh/docs/examples/14-agent-live-smoke-gemini/

Run a model-backed action against Gemini's OpenAI-compatible endpoint.

The composition resolves a Gemini seat through `Route.openaiChatCompatible` and runs the agent's structured completion loop. `main` accepts the endpoint and API key; its key defaults to `GEMINI_API_KEY`.

A successful run depends on provider access, quota, and the configured model. Use the scripted-model examples when testing without a network request.

## Run the test

The complete program is [`examples/src/14-agent-live-smoke-gemini.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/14-agent-live-smoke-gemini.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

This program has no dedicated test file. Read its setup requirements before running it; importing an example alone may only define its exports.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/engine`](/docs/reference/api/engine/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/14-agent-live-smoke-gemini.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as FlowEngineLike from "@smthrs/agent/FlowEngineLike"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { FlowEngine } from "@smthrs/engine"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as GrantStore from "@smthrs/kernel/GrantStore"
import * as KernelHttpClient from "@smthrs/kernel/HttpClient"
import * as RequestExecutor from "@smthrs/model/RequestExecutor"
import * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import { Effect, Layer, Option, Redacted, Schema } from "effect"

/**
 * The real HTTP transport: the kernel's guarded client (always-allow, since
 * this is a standalone smoke test with no grant policy to enforce) over
 * Node's real undici-backed client.
 *
 * @category layers
 * @since 0.1.0
 */
export const executorLayer = RequestExecutor.layer.pipe(
  Layer.provide(KernelHttpClient.layer),
  Layer.provide(GrantStore.layerNoop),
  Layer.provide(NodeHttpClient.layerUndici)
)

/**
 * A real `SeatResolver`: any `gemini:<modelId>` seat resolves to a live
 * Gemini route over its OpenAI-compatible Chat Completions endpoint, using
 * the real `GEMINI_API_KEY` from the environment as the bearer token.
 *
 * @category layers
 * @since 0.1.0
 */
export const liveGeminiSeats = (baseUrl: string, apiKey: string) =>
  Layer.effect(SeatResolver.SeatResolver)(
    Effect.gen(function*() {
      const executor = yield* RequestExecutor.RequestExecutor
      return SeatResolver.make({
        resolve: (id) =>
          Effect.gen(function*() {
            const modelId = Seat.modelIdOf(id)
            const routeConfig = yield* Effect.fromResult(
              Route.openaiChatCompatible({
                id: "gemini",
                baseUrl,
                path: "/chat/completions",
                apiKey: Redacted.make(apiKey)
              })
            ).pipe(
              Effect.mapError((error) => new Seat.SeatUnresolved({ seat: id, message: error.message }))
            )
            const model = yield* Route.toModel(routeConfig).pipe(
              Effect.provideService(RequestExecutor.RequestExecutor, executor)
            )
            return Seat.make({
              id,
              model,
              route: FlowEngineLike.routeResolver(routeConfig),
              contextWindowTokens: SeatResolver.contextWindowTokensFor(modelId)
            })
          })
      })
    })
  ).pipe(Layer.provide(executorLayer))

/** One model-backed step: answer a question in one short sentence. */
export const LiveSmokeGemini = AgentAction.make("examples/LiveSmokeGemini", {
  payload: { question: Schema.String },
  output: Schema.Struct({ answer: Schema.String }),
  seat: "gemini:gemini-2.5-flash",
  system: ["You are a terse assistant. Answer in one short sentence and nothing else."],
  prompt: ({ question }) => question
})

/** The workflow: one step, so the live round trip to Gemini is the only thing under test. */
export const LiveSmokeGeminiWorkflow = Flow.make("examples/LiveSmokeGeminiWorkflow", {
  payload: { question: Schema.String },
  success: Schema.Struct({ answer: Schema.String }),
  error: AgentAction.AgentFailure,
  body: ({ question }) => LiveSmokeGemini.call({ question })
})

const host = AgentAction.layerHost({
  registry: Registry.makeNoop({
    list: () => Effect.succeed([]),
    visible: () => Effect.succeed([]),
    getOption: () => Effect.succeed(Option.none())
  }),
  limits: { calls: 1 },
  capabilityEnvelope: [],
  maxFrames: 1
})

/**
 * Builds the full live composition against a Gemini endpoint. Takes
 * `baseUrl`/`apiKey` explicitly rather than hardcoding them.
 *
 * @category layers
 * @since 0.1.0
 */
export const liveLayer = (baseUrl: string, apiKey: string) =>
  Layer.mergeAll(LiveSmokeGemini.layer, Interpreter.layer(LiveSmokeGeminiWorkflow)).pipe(
    Layer.provideMerge(Layer.mergeAll(host, liveGeminiSeats(baseUrl, apiKey), Agent.layer)),
    // Gemini's reset-bearing refusals should park. This standalone smoke run
    // has no approved plan envelope from which to derive a spend ceiling.
    // eslint-disable-next-line no-restricted-syntax -- this standalone smoke has no approved envelope
    Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerDefault(), Budget.layerUnbounded())),
    Layer.provideMerge(Agent.layerDefaults),
    Layer.provideMerge(Action.layerImplementations),
    Layer.provideMerge(FlowEngine.layerMemory),
    Layer.provideMerge(NodeCrypto.layer)
  )

/**
 * Runs the workflow against a live Gemini seat.
 *
 * @category constructors
 * @since 0.1.0
 */
export const main = (
  question: string,
  baseUrl = "https://generativelanguage.googleapis.com/v1beta/openai",
  apiKey = process.env.GEMINI_API_KEY ?? ""
) =>
  LiveSmokeGeminiWorkflow.execute(
    { question },
    { executionId: `live-smoke-gemini-${Date.now()}` }
  ).pipe(
    Effect.orDie,
    Effect.provide(liveLayer(baseUrl, apiKey))
  )

if (import.meta.url === `file://${process.argv[1]}`) {
  Effect.runPromise(main("What is the capital of France? Answer in one word.")).then(
    (result) => {
      console.log("RESULT:", JSON.stringify(result))
    },
    (error) => {
      console.error("FAILED:", error)
      process.exitCode = 1
    }
  )
}
```

---

# Model layer smoke
https://smithers.sh/docs/examples/15-model-layer-smoke/

Stream a response directly through the model package.

`ask` accepts a question, model ID, compatible endpoint, and API key. It constructs a route, calls `Model.stream`, and collects response text. This isolates model transport from the agent's cell loop and structured completion protocol.

Running the file directly uses its command-line arguments and contacts the selected endpoint. The defaults target a local model server.

## Run the test

The complete program is [`examples/src/15-model-layer-smoke.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/15-model-layer-smoke.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

This program has no dedicated test file. Read its setup requirements before running it; importing an example alone may only define its exports.

## Related reference

[`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/model`](/docs/reference/api/model/)

## Source

```ts title="examples/src/15-model-layer-smoke.ts"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import * as GrantStore from "@smthrs/kernel/GrantStore"
import * as KernelHttpClient from "@smthrs/kernel/HttpClient"
import { Message, ModelRequest } from "@smthrs/model/ModelRequest"
import * as RequestExecutor from "@smthrs/model/RequestExecutor"
import * as Route from "@smthrs/model/Route"
import { Effect, Layer, Redacted, Stream } from "effect"

/**
 * The real HTTP transport: the kernel's guarded client (always-allow, since
 * this is a standalone smoke test with no grant policy to enforce) over
 * Node's real undici-backed client.
 *
 * @category layers
 * @since 0.1.0
 */
export const executorLayer = RequestExecutor.layer.pipe(
  Layer.provide(KernelHttpClient.layer),
  Layer.provide(GrantStore.layerNoop),
  Layer.provide(NodeHttpClient.layerUndici)
)

/**
 * Sends one prompt to a real `openaiChatCompatible` endpoint and returns the
 * concatenated text of the response, with nothing but `packages/smithers/agent/model`
 * between the caller and the wire.
 *
 * @category constructors
 * @since 0.1.0
 */
export const ask = (question: string, modelId: string, baseUrl: string, apiKey: string) =>
  Effect.gen(function*() {
    const routeConfig = yield* Effect.fromResult(Route.openaiChatCompatible({ id: "smoke", baseUrl, apiKey: Redacted.make(apiKey) }))
    const model = yield* Route.toModel(routeConfig)
    const request = ModelRequest.make({
      modelId,
      system: [],
      messages: [Message.user(question)],
      tools: [],
      params: {}
    })
    const chunks: Array<string> = []
    yield* model.stream(request).pipe(
      Stream.runForEach((event) =>
        Effect.sync(() => {
          if (event.type === "text-delta") chunks.push(event.text)
        })
      )
    )
    return chunks.join("")
  }).pipe(Effect.provide(executorLayer))

if (import.meta.url === `file://${process.argv[1]}`) {
  const [, , modelId = "qwen2.5-coder:1.5b", baseUrl = "http://localhost:11434", apiKey = "local"] = process.argv
  Effect.runPromise(ask("What is the capital of France? Answer in one word.", modelId, baseUrl, apiKey)).then(
    (answer) => {
      console.log("ANSWER:", answer)
    },
    (error) => {
      console.error("FAILED:", error)
      process.exitCode = 1
    }
  )
}
```

---

# Fan out fan in
https://smithers.sh/docs/examples/16-fan-out-fan-in/

Run checks in batches of two and join their verdicts into a report.

Each batch is a `Node.all`. A dependency on the previous batch keeps later checks from starting early. Priority chooses which ready checks run first without changing their result identity.

A collecting action receives the planned verdicts as payload fields and builds the report at execution time. Driving the same execution again reuses the recorded checks. The second half loads equivalent declarations from the example's project directory.

## Run the test

The complete program is [`examples/src/16-fan-out-fan-in.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/16-fan-out-fan-in.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/16-fan-out-fan-in.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/16-fan-out-fan-in.ts"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as NodePath from "@effect/platform-node/NodePath"
import { Action, Flow, type FlowRuntime, Graph, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import { Node } from "@smthrs/plan"
import type * as Planned from "@smthrs/plan/Planned"
import { Executable } from "@smthrs/registry"
import type * as Crypto from "effect/Crypto"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import gateModule from "./16-project/flows/gate/flow.ts"
import { durableEngine } from "./durable-layer.ts"

/**
 * One check, and the verdict it reports.
 *
 * `after` carries the verdicts the previous batch reported. It is declared
 * rather than ambient because it is the dependency edge: a check that names no
 * predecessor is free to start immediately, and one that names a batch waits
 * for it. The first batch passes `null`.
 */
export const Check = Action.make("examples/Check", {
  payload: {
    name: Schema.String,
    target: Schema.String,
    after: Schema.Json
  },
  success: Schema.String
})

/** The fan-in step: five verdicts arrive as payload fields, one report leaves. */
export const Collect = Action.make("examples/Collect", {
  payload: {
    lint: Schema.String,
    types: Schema.String,
    unit: Schema.String,
    audit: Schema.String,
    licence: Schema.String
  },
  success: Schema.String
})

/** One declared check: what it is called and how urgent it is. */
export interface CheckSpec {
  readonly name: string
  readonly priority: number
}

/**
 * The gate's checks, in declaration order.
 *
 * `audit` blocks a release and `licence` is nearly as urgent, so both carry a
 * priority. The other three state none and keep declaration order behind them.
 */
export const specs: ReadonlyArray<CheckSpec> = [
  { name: "lint", priority: 0 },
  { name: "types", priority: 0 },
  { name: "unit", priority: 0 },
  { name: "audit", priority: 9 },
  { name: "licence", priority: 5 }
]

/**
 * Splits checks into batches of at most `concurrency`, highest priority first
 * and declaration order among equals.
 *
 * The sort is total, so a plan built twice from the same list is identical.
 * That matters, because the batch a check lands in is part of the topology the
 * step keys are derived from.
 *
 * The bound is checked before anything is sorted, because the loop below
 * advances by it: zero never advances, a negative moves away from termination,
 * and a fractional or non-finite bound groups checks the doc line above does
 * not describe. The caller here states a literal, so a refusal is a defect in
 * this example rather than an error to handle, which is why it throws.
 */
export const batches = (
  checks: ReadonlyArray<CheckSpec>,
  concurrency: number
): ReadonlyArray<ReadonlyArray<string>> => {
  if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
    throw new Error(`batch concurrency must be a positive integer: ${concurrency}`)
  }
  const order = checks
    .map((spec, index) => ({ spec, index }))
    .sort((left, right) => right.spec.priority - left.spec.priority || left.index - right.index)
    .map((entry) => entry.spec.name)
  const grouped: Array<ReadonlyArray<string>> = []
  for (let offset = 0; offset < order.length; offset += concurrency) {
    grouped.push(order.slice(offset, offset + concurrency))
  }
  return grouped
}

/** The batches the gate declares, before anything runs. */
export const declaredBatches: ReadonlyArray<ReadonlyArray<string>> = batches(specs, 2)

const priorityOf = (name: string): number => specs.find((spec) => spec.name === name)?.priority ?? 0

/** The requirements the gate's two actions carry. */
type GateRequirements = Action.Requirement<"examples/Check" | "examples/Collect">

/**
 * The gate's topology, given the thing being gated.
 *
 * It is a plain function so the two declarations below can share it: one takes
 * a target directly, the other takes the envelope a discovered descriptor is
 * invoked with. A body is an ordinary function of its payload, so "the same
 * gate under a different payload" needs no indirection beyond this.
 */
const gateBody = (target: string): Node.Node<string, never, GateRequirements> => {
  const stage = (
    index: number,
    after: Schema.Json,
    collected: Readonly<Record<string, Planned.Planned<string>>>
  ): Node.Node<string, never, GateRequirements> => {
    const batch = declaredBatches[index]
    if (batch === undefined) {
      return Collect.call({
        lint: collected.lint!,
        types: collected.types!,
        unit: collected.unit!,
        audit: collected.audit!,
        licence: collected.licence!
      })
    }
    const members: Record<string, Node.Node<string, never, Action.Requirement<"examples/Check">>> = {}
    for (const name of batch) {
      members[name] = Node.priority(Check.call({ name, target, after }), priorityOf(name))
    }
    return Node.bindPlanned(
      Node.all(members),
      (verdicts: Planned.Planned<Readonly<Record<string, string>>>) => {
        const next: Record<string, Planned.Planned<string>> = { ...collected }
        const fields = verdicts as unknown as Readonly<Record<string, Planned.Planned<string>>>
        for (const name of batch) next[name] = fields[name]!
        return stage(index + 1, verdicts as unknown as Schema.Json, next)
      }
    )
  }
  return stage(0, null, {})
}

/** The release gate: five checks, two at a time, one report. */
export const Gate = Flow.make("examples/Gate", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: ({ target }: { readonly target: string }) => gateBody(target)
})

/**
 * The delegate the discovered gate runs on.
 *
 * A discovered descriptor says WHAT should run; the host says HOW, by
 * registering a flow under the name the descriptor delegates to. The payload is
 * `Executable.Invocation` rather than the gate's own schema because one
 * delegate serves many descriptors: the envelope carries the caller's input,
 * the descriptor's name, and the decisions the bridge lowered off it.
 */
export const GateRunner = Flow.make("examples/GateRunner", {
  payload: Executable.Invocation,
  success: Schema.String,
  body: (invocation: Executable.Invocation) =>
    gateBody((invocation.input as { readonly target?: string } | null)?.target ?? "release")
})

/** The project whose `flows/` directory declares the gate. */
export const projectRoot: string = join(dirname(fileURLToPath(import.meta.url)), "16-project")

/** The name discovery derives for `flows/gate/flow.ts` from its directory. */
export const discoveredFlow = "gate"

/** The priority the declaration on disk carries. */
export const declaredOnDiskPriority = 7

/**
 * How the discovered descriptor is loaded and what it may delegate to.
 *
 * `load` is supplied rather than left to the bridge's default dynamic import
 * because this example runs under a TypeScript-aware runner: a static import is
 * the same module the default loader would produce, without asking the runtime
 * to evaluate a `.ts` file on its own. A packaged host keeps the default.
 */
const bridge: Executable.Options = {
  delegates: [GateRunner],
  load: () => Effect.succeed({ default: gateModule })
}

/** The platform services discovery and body loading read the project through. */
const platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)

/** `<projectRoot>/flows/**`, scanned, with bodies still unread. */
const registry = Executable.layerProject({ root: projectRoot }).pipe(Layer.provide(platform), Layer.orDie)

/**
 * Starts the bridged flow.
 *
 * A bridged flow declares open requirements, because the bridge cannot know at
 * the type level what the delegate a descriptor names will need. The launch is
 * narrowed here rather than letting `any` widen every effect downstream.
 */
const start = (
  executable: Executable.Executable,
  target: string,
  executionId: string
): Effect.Effect<string, never, FlowRuntime.FlowRuntime | Crypto.Crypto> =>
  (executable.flow.execute({ input: { target } }, { executionId }) as Effect.Effect<
    string,
    unknown,
    FlowRuntime.FlowRuntime | Crypto.Crypto
  >).pipe(Effect.orDie)

/** What one run of the gate the project declared observed. */
export interface DiscoveredSummary {
  /** The descriptor name discovery derived from the directory. */
  readonly flow: string
  /** The flow the descriptor delegates to. */
  readonly delegate: string
  /** The priority the bridge lowered off the declaration. */
  readonly lowered: number | undefined
  /** Every priority the built plan's nodes carry, in plan order. */
  readonly planned: ReadonlyArray<number>
  /** The report the discovered gate produced. */
  readonly report: string
  /** The most checks that were ever running at the same moment. */
  readonly maxInFlight: number
}

/**
 * Runs the gate the project declared on disk.
 *
 * Nothing below names the gate: discovery finds `flows/gate/flow.ts`, the
 * bridge lowers the priority that file declares onto the delegating node, and
 * the plan the engine drives is the plan `main` builds: the same five checks,
 * the same bound of two, the same report.
 */
export const discovered = (filename: string): Effect.Effect<DiscoveredSummary> =>
  Effect.gen(function*() {
    let inFlight = 0
    let maxInFlight = 0

    const check = Check.toLayer(({ name }) =>
      Effect.gen(function*() {
        inFlight += 1
        maxInFlight = Math.max(maxInFlight, inFlight)
        yield* Effect.sleep("25 millis")
        inFlight -= 1
        return `${name}:clean`
      })
    )

    const collect = Collect.toLayer((verdicts) =>
      Effect.succeed(
        [verdicts.lint, verdicts.types, verdicts.unit, verdicts.audit, verdicts.licence].join(" ")
      )
    )

    const executable = yield* Executable.fromRegistry(discoveredFlow, bridge).pipe(
      Effect.provide(Layer.merge(registry, platform)),
      Effect.orDie
    )

    // The plan, before anything runs. The delegating node carries the priority
    // the file declared, beside the priorities the body states itself.
    const planned = Graph.nodes(Graph.build(executable.flow, { input: { target: "release" } }))
      .map((node) => node.draft.priority)
      .filter((priority): priority is number => priority !== undefined)

    const report = yield* Effect.scoped(
      start(executable, "release", "gate-discovered").pipe(
        Effect.provide(
          Layer.mergeAll(check, collect, Interpreter.layer(GateRunner), executable.layer).pipe(
            Layer.provideMerge(Action.layerImplementations),
            Layer.provideMerge(durableEngine(filename, "examples-gate-discovered"))
          )
        )
      )
    )

    return {
      flow: executable.descriptor.name,
      delegate: executable.delegate,
      lowered: executable.lowered.priority,
      planned,
      report,
      maxInFlight
    } satisfies DiscoveredSummary
  }).pipe(Effect.orDie)

/** The priority each check carries in the built plan, by check name. */
export const declaredPriorities = (target = "release"): Readonly<Record<string, number | undefined>> => {
  const found: Record<string, number | undefined> = {}
  for (const node of Graph.nodes(Graph.build(Gate, { target }))) {
    if (node.kind !== "ActionCall") continue
    const payload = node.payload as { readonly name?: unknown } | undefined
    if (typeof payload?.name === "string") found[payload.name] = node.draft.priority
  }
  return found
}

/** What one run of the gate observed. */
export interface Summary {
  /** The report the fan-in step produced. */
  readonly report: string
  /** The report a re-drive of the same execution id produced. */
  readonly replayed: string
  /** The order the checks started in. */
  readonly started: ReadonlyArray<string>
  /** The most checks that were ever running at the same moment. */
  readonly maxInFlight: number
  /** How many times each check's body ran, across BOTH executions. */
  readonly dispatches: Readonly<Record<string, number>>
  /** The distinct lifecycle events the run journalled. */
  readonly eventTypes: ReadonlyArray<string>
}

/** Runs the gate over one SQLite file and reports what the fan-out did. */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const started: Array<string> = []
    const dispatches: Record<string, number> = {}
    let inFlight = 0
    let maxInFlight = 0

    const check = Check.toLayer(({ name }) =>
      Effect.gen(function*() {
        started.push(name)
        dispatches[name] = (dispatches[name] ?? 0) + 1
        inFlight += 1
        maxInFlight = Math.max(maxInFlight, inFlight)
        // Long enough that two members of one batch genuinely overlap, short
        // enough that the suite stays quick. Without a suspension point the
        // members would settle one after another and the bound would be
        // unexercised rather than merely untested.
        yield* Effect.sleep("25 millis")
        inFlight -= 1
        return `${name}:clean`
      })
    )

    const collect = Collect.toLayer((verdicts) =>
      Effect.succeed(
        [verdicts.lint, verdicts.types, verdicts.unit, verdicts.audit, verdicts.licence].join(" ")
      )
    )

    const observed = yield* Effect.scoped(
      Effect.gen(function*() {
        const report = yield* Gate.execute({ target: "release" }, { executionId: "gate-1" })
        // The same execution id, so this is a re-drive rather than a second
        // gate. Each fan-out member's verdict is already recorded against its
        // own step key, so the batch is rebuilt from durable state and no check
        // dispatches again.
        const replayed = yield* Gate.execute({ target: "release" }, { executionId: "gate-1" })
        const journal = yield* Journal.Journal
        yield* journal.flush
        const page = yield* journal.entries({ runId: "gate-1" as JournalEvent.RunId, limit: 500 })
        return { report, replayed, eventTypes: [...new Set(page.entries.map((entry) => entry.eventType))] }
      }).pipe(
        Effect.provide(
          Layer.mergeAll(check, collect, Interpreter.layer(Gate)).pipe(
            Layer.provideMerge(Action.layerImplementations),
            Layer.provideMerge(durableEngine(filename, "examples-gate"))
          )
        )
      )
    )

    return {
      report: observed.report,
      replayed: observed.replayed,
      started,
      maxInFlight,
      dispatches,
      eventTypes: observed.eventTypes
    } satisfies Summary
  }).pipe(Effect.orDie)
```

---

# Review loop
https://smithers.sh/docs/examples/17-review-loop/

Draft, review, and revise until approval or the round limit.

The plan contains the bounded review rounds and their branches before execution. A runtime verdict chooses which branch settles. The reviewer is a deterministic action; a model-backed action can use the same graph shape.

The example verifies both approval before the limit and termination when the available rounds are exhausted.

## Run the test

The complete program is [`examples/src/17-review-loop.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/17-review-loop.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/17-review-loop.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/plan`](/docs/reference/api/plan/)

## Source

```ts title="examples/src/17-review-loop.ts"
import { Action, Flow, Graph, Interpreter } from "@smthrs/flow"
import { Node } from "@smthrs/plan"
import type * as Planned from "@smthrs/plan/Planned"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** How many reviews one run may spend. The plan holds exactly this many. */
export const maxRounds = 3

/** The first draft. */
export const Draft = Action.make("examples/Draft", {
  payload: { topic: Schema.String },
  success: Schema.String
})

/** The reviewer's verdict: whether it passes, and what to fix if it does not. */
export const Verdict = Schema.Struct({
  approved: Schema.Boolean,
  feedback: Schema.String
})

/** One review of one draft. */
export const Review = Action.make("examples/Review", {
  payload: { text: Schema.String },
  success: Verdict
})

/** One revision, given the draft and the feedback that rejected it. */
export const Revise = Action.make("examples/Revise", {
  payload: { text: Schema.String, feedback: Schema.String },
  success: Schema.String
})

/** What the loop settles with: the text, whether it passed, and when. */
export const Outcome = Schema.Struct({
  text: Schema.String,
  approved: Schema.Boolean,
  rounds: Schema.Number
})

/** The terminal step. Both exits go through it, so both are in the plan. */
export const Publish = Action.make("examples/Publish", {
  payload: { text: Schema.String, approved: Schema.Boolean, rounds: Schema.Number },
  success: Outcome
})

type LoopRequirements = Action.Requirement<
  "examples/Draft" | "examples/Review" | "examples/Revise" | "examples/Publish"
>

/**
 * One round: review the text in hand, publish it if it passed, publish it
 * unapproved if this was the last planned round, and otherwise revise and
 * recurse.
 *
 * `text` is a planned value, so it is passed into payload fields and never read
 * here. The predicate is the only place a real value is inspected, and it runs
 * at execution time on the verdict the reviewer actually returned.
 */
const reviewRound = (
  index: number,
  text: Planned.Planned<string>
): Node.Node<typeof Outcome.Type, never, LoopRequirements> =>
  Node.branch(Review.call({ text }), {
    if: (verdict) => verdict.approved,
    then: () => Publish.call({ text, approved: true, rounds: index }),
    else: index >= maxRounds
      ? () => Publish.call({ text, approved: false, rounds: index })
      : (verdict) =>
        Node.bindPlanned(
          Revise.call({ text, feedback: verdict.feedback }),
          (revised: Planned.Planned<string>) => reviewRound(index + 1, revised)
        )
  })

/** The bounded review loop. */
export const Article = Flow.make("examples/Article", {
  payload: { topic: Schema.String },
  success: Outcome,
  body: ({ topic }: { readonly topic: string }) =>
    Node.bindPlanned(Draft.call({ topic }), (text: Planned.Planned<string>) => reviewRound(1, text))
})

/** How many nodes of each action the built plan holds, before anything runs. */
export const declaredCalls = (topic = "durable loops"): Readonly<Record<string, number>> => {
  const counted: Record<string, number> = {}
  for (const node of Graph.nodes(Graph.build(Article, { topic }))) {
    if (node.kind !== "ActionCall") continue
    const action = (node.ast as { readonly action?: unknown }).action
    if (typeof action === "string") counted[action] = (counted[action] ?? 0) + 1
  }
  return counted
}

/** What one run of the loop observed. */
export interface Summary {
  /** The outcome the terminal step published. */
  readonly outcome: typeof Outcome.Type
  /** Every review the run performed, in order, as `text -> approved`. */
  readonly reviews: ReadonlyArray<string>
  /** Every revision the run performed, in order. */
  readonly revisions: ReadonlyArray<string>
}

/**
 * Runs the loop against a reviewer that approves on `approveOnRound`.
 *
 * Passing a round past the budget is how the second scenario is produced: the
 * reviewer never approves, so the loop ends on the planned exhausted exit
 * rather than on approval.
 */
export const main = (
  filename: string,
  options: { readonly approveOnRound: number; readonly executionId: string }
): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const reviews: Array<string> = []
    const revisions: Array<string> = []
    let seen = 0

    const draft = Draft.toLayer(({ topic }) => Effect.succeed(`draft of ${topic}`))

    const review = Review.toLayer(({ text }) =>
      Effect.sync(() => {
        seen += 1
        const approved = seen >= options.approveOnRound
        reviews.push(`${text} -> ${approved}`)
        return { approved, feedback: `tighten round ${seen}` }
      })
    )

    const revise = Revise.toLayer(({ feedback, text }) =>
      Effect.sync(() => {
        const revised = `${text} (${feedback})`
        revisions.push(revised)
        return revised
      })
    )

    const publish = Publish.toLayer((outcome) => Effect.succeed(outcome))

    const outcome = yield* Effect.scoped(
      Article.execute({ topic: "durable loops" }, { executionId: options.executionId }).pipe(
        Effect.provide(
          Layer.mergeAll(draft, review, revise, publish, Interpreter.layer(Article)).pipe(
            Layer.provideMerge(Action.layerImplementations),
            Layer.provideMerge(durableEngine(filename, "examples-review"))
          )
        )
      )
    )

    return { outcome, reviews, revisions } satisfies Summary
  }).pipe(Effect.orDie)
```

---

# Approval and signal
https://smithers.sh/docs/examples/18-approval-and-signal/

Compare plan approval, an in-run approval, and a signal wait.

Plan approval authorizes a reviewed launch payload. The `Clearance` action registers an in-run token and parks until a decision exists. A later `WaitFor` action parks for a named event.

`Control.signal` records a signal. The example's host bridge, `deliverSignals`, turns it into completion of the matching wait point. The run's waiting reason distinguishes the approval token from the event wait.

## Run the test

The complete program is [`examples/src/18-approval-and-signal.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/18-approval-and-signal.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/18-approval-and-signal.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/18-approval-and-signal.ts"
import { Control, ControlLive, ControlRuntime, SqlControlRuntime } from "@smthrs/control"
import * as ControlExecutor from "@smthrs/control/ControlExecutor"
import type * as ControlSchema from "@smthrs/control/ControlSchema"
import { Action, DurableDeferred, Flow, FlowRuntime, Interpreter, WaitFor } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import { NotificationQueue } from "@smthrs/notifications"
import { Node } from "@smthrs/plan"
import { Registry } from "@smthrs/registry"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** The empty capability envelope this flow is planned under. */
const envelope: ControlSchema.Envelope = { capabilities: [], flows: [], budget: {} }

/** The id the clearance step registers its approval token under. */
export const clearanceRequestId = "ship-clearance"

/**
 * What the clearance request is a decision ABOUT.
 *
 * A node approval carries a digest for the same reason a plan approval does: it
 * pins what was reviewed, so a decision cannot be re-aimed at a different
 * request afterwards. It is deliberately not the plan's digest. The two gates
 * are separate mechanisms and a run that was never planned still has steps
 * worth gating.
 */
export const clearanceDigest = "examples/Ship:clearance"

/** The exact approval request the step registers and an operator decides. */
export const clearanceRequest = (
  runId: string
): Extract<ControlSchema.ApprovalTarget, { readonly _tag: "Node" }> => ({
  _tag: "Node",
  runId: runId as ControlSchema.RunId,
  requestId: clearanceRequestId,
  digest: clearanceDigest,
  envelope
})

/**
 * The wait point the clearance park awaits.
 *
 * Nothing ever completes it, which is the point: an approval is not a value
 * arriving, it is a decision recorded somewhere else. The wake is the operator
 * re-driving the run, and the step reads the decision off the token rather than
 * off this deferred.
 */
const clearanceGate = DurableDeferred.make(`Approval/${clearanceRequestId}`, { success: Schema.Json })

/**
 * The in-run approval gate, as an ordinary step.
 *
 * It is the step that registers the token, not the caller, so the request
 * exists exactly when a run has reached the thing that needs deciding.
 */
export const Clearance = Action.make("examples/Clearance", {
  payload: { requestId: Schema.String },
  success: Schema.String,
  error: Schema.Union([ControlRuntime.ApprovalPending, ControlRuntime.ApprovalDenied])
})

/** The flow both halves of the example are about. */
export const Ship = Flow.make("examples/Ship", {
  payload: { build: Schema.String },
  success: Schema.Json,
  error: Schema.Union([WaitFor.WaitForRequestInvalid, ControlRuntime.ApprovalPending, ControlRuntime.ApprovalDenied]),
  body: () =>
    Node.andThen(
      Clearance.call({ requestId: clearanceRequestId }),
      WaitFor.action.call({ name: "ship" })
    )
})

/** The wait point the run parks on, and the resolver's half of it. */
export const gate = WaitFor.deferred("ship")

/** The engine run the node approval and the signal are aimed at. */
export const shipRunId = "examples-ship"

/**
 * The control-plane flow catalog.
 *
 * `Control.plan` answers `FlowNotFound` for a flow the runtime does not know,
 * so a plan gate needs the flow declared here as well as registered with the
 * engine. The two catalogs are separate on purpose: one is what may be planned,
 * the other is what may be executed.
 */
const flows: ReadonlyArray<ControlRuntime.MemoryFlow> = [
  {
    flowId: "examples/Ship" as ControlSchema.FlowId,
    description: "Ships a build once somebody says so",
    deployClass: false,
    envelope
  }
]

/**
 * Completes every `WaitFor` wait point named by a signal the control plane has
 * admitted for this run.
 *
 * This is the seam a deployment owns. `Control.signal` writes the fact; nothing
 * in the control plane decides that a given signal ends a given wait, because
 * only the flow author knows which wait points exist and what value they carry.
 * Naming the wait point after the signal is this example's convention, and a
 * host with several conventions writes several of these.
 */
export const deliverSignals = (
  runId: string
): Effect.Effect<
  ReadonlyArray<string>,
  never,
  ControlRuntime.ControlRuntime | FlowRuntime.FlowRuntime
> =>
  Effect.gen(function*() {
    const runtime = yield* ControlRuntime.ControlRuntime
    const signals = yield* runtime.deliveredSignals(runId as ControlSchema.RunId)
    const delivered: Array<string> = []
    for (const signal of signals) {
      const waitPoint = WaitFor.deferred(signal.name)
      yield* DurableDeferred.succeed(waitPoint, {
        token: DurableDeferred.tokenFromExecutionId(waitPoint, { flow: Ship, executionId: runId }),
        value: signal.payload
      })
      delivered.push(signal.name)
    }
    return delivered
  }).pipe(Effect.orDie)

/** What the plan gate did. */
export interface PlanGate {
  /** The receipt `run` answered before anybody approved the plan. */
  readonly beforeApproval: string
  /** The status that receipt reported. */
  readonly beforeApprovalStatus: string
  /** The receipt `run` answered once the plan was approved. */
  readonly afterApproval: string
  /** The decision the stored plan carries after approval. */
  readonly decision: string
  /** The decision the second, denied plan carries. */
  readonly deniedDecision: string
  /** The failure `run` answered for the denied plan. */
  readonly deniedLaunch: string
  /** How many launches the executor was handed. */
  readonly launches: number
}

/** What the in-run gates did. */
export interface RunGates {
  /** The status the run had after the drive that reached the clearance step. */
  readonly firstPark: string
  /** What the engine recorded that first park as waiting for. */
  readonly firstWaitingFor: string | undefined
  /** What the clearance step read off its token, once per time it ran. */
  readonly clearanceReads: ReadonlyArray<ControlRuntime.ApprovalDecision["_tag"]>
  /** The status the control plane reported for the parked run. */
  readonly parked: string
  /** What the engine recorded the run as waiting for. */
  readonly waitingFor: string | undefined
  /** The receipt the node approval answered. */
  readonly approvalReceipt: string
  /** The actual durable decision, not whether either decision was recorded. */
  readonly approvalDecision: ControlRuntime.ApprovalDecision["_tag"]
  /** A separate run is denied at the same clearance gate. */
  readonly deniedClearanceReads: ReadonlyArray<ControlRuntime.ApprovalDecision["_tag"]>
  readonly deniedFailure: string
  /** The approval decisions the run's journal carries, by target. */
  readonly approvals: ReadonlyArray<string>
  /** The signals the control plane admitted for the run. */
  readonly signals: ReadonlyArray<string>
  /** The wait points the host completed from those signals. */
  readonly delivered: ReadonlyArray<string>
  /** The value the run finished with, which is the signal's payload. */
  readonly result: unknown
}

/** Everything one pass observed. */
export interface Summary {
  readonly plan: PlanGate
  readonly run: RunGates
}

const receipt = (value: { readonly _tag: string }): string => value._tag

/** Runs both gates over one SQLite file. */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let launches = 0
    const clearanceReads: Array<ControlRuntime.ApprovalDecision["_tag"]> = []

    /**
     * The clearance step's implementation: register, read, park or proceed.
     *
     * `registerApproval` is the whole gate. It creates the token the first time
     * and answers the existing one afterwards, so this handler asks the same
     * question on every attempt and gets a different answer once somebody has
     * decided. Parking rather than failing is the honest shape: the run has
     * done nothing wrong, it is waiting for a person.
     */
    const clearance = Clearance.toLayer(({ requestId }) =>
      Effect.gen(function*() {
        const instance = yield* FlowRuntime.FlowInstance
        const runtime = yield* ControlRuntime.ControlRuntime
        const request = clearanceRequest(instance.executionId)
        let token = yield* Effect.orDie(runtime.registerApproval(request))
        clearanceReads.push(token._tag)
        if (token._tag === "Pending") {
          // Waking a wait point is not approval. Re-read the durable decision
          // if this deferred is completed explicitly instead of parking.
          yield* FlowRuntime.annotateWaiting({ reason: "approval", token: requestId })
          yield* DurableDeferred.await(clearanceGate)
          token = yield* Effect.orDie(runtime.registerApproval(request))
        }
        return (yield* ControlRuntime.requireApproved(token)).tokenId
      })
    )

    /**
     * The acceptance port. It records the launch and answers `pending`, which
     * is the honest answer for a plane whose executor is a queue somebody else
     * drains. An executor that started the run would answer `accepted`.
     */
    const executor = ControlExecutor.layer(
      ControlExecutor.makeNoop({
        launch: () =>
          Effect.sync(() => {
            launches += 1
            return "pending" as const
          })
      })
    )

    const controlPlane = ControlLive.layer.pipe(
      Layer.provideMerge(
        Layer.mergeAll(
          SqlControlRuntime.layer({
            flows,
            owner: { hostId: "examples-approval", pid: 1, nonce: "approval" }
          }).pipe(Layer.orDie),
          NotificationQueue.layer,
          executor,
          Registry.layerNoop()
        )
      )
    )

    // One database beneath both, so the plane steers the runs the engine owns.
    const plane = controlPlane.pipe(Layer.provideMerge(durableEngine(filename, "examples-approval")))

    // The clearance step reads the control plane, so the registrations are
    // built OVER the plane rather than beside it. That is the dependency the
    // gate is: a step that consults a decision needs the service that holds it.
    const stack = Layer.mergeAll(clearance, WaitFor.layer, Interpreter.layer(Ship)).pipe(
      Layer.provideMerge(Action.layerImplementations),
      Layer.provideMerge(plane)
    )

    return yield* Effect.scoped(
      Effect.gen(function*() {
        const control = yield* Control.Control
        const runtime = yield* ControlRuntime.ControlRuntime

        // ------------------------------------------------------- the plan gate
        const card = yield* control.plan({
          flowId: "examples/Ship" as ControlSchema.FlowId,
          input: { build: "v1.4.0" }
        })
        const launch = {
          _tag: "Plan" as const,
          planId: card.planId,
          digest: card.digest,
          envelope: card.envelope
        }
        // A pending plan does not launch. The receipt says why.
        const beforeApproval = yield* control.run({
          ...launch,
          idempotencyKey: "run:before" as ControlSchema.IdempotencyKey
        })
        yield* control.approve({
          target: { _tag: "Plan", planId: card.planId, digest: card.digest, envelope: card.envelope },
          scope: card.approval.scope,
          idempotencyKey: "approve:plan" as ControlSchema.IdempotencyKey
        })
        const afterApproval = yield* control.run({
          ...launch,
          idempotencyKey: "run:after" as ControlSchema.IdempotencyKey
        })
        const approved = yield* runtime.getPlan(card.planId)

        // A second plan, denied. The launch refuses rather than parking.
        const rejected = yield* control.plan({
          flowId: "examples/Ship" as ControlSchema.FlowId,
          input: { build: "v1.4.1" }
        })
        yield* control.deny({
          target: {
            _tag: "Plan",
            planId: rejected.planId,
            digest: rejected.digest,
            envelope: rejected.envelope
          },
          scope: rejected.approval.scope,
          idempotencyKey: "deny:plan" as ControlSchema.IdempotencyKey
        })
        const deniedPlan = yield* runtime.getPlan(rejected.planId)
        const deniedLaunch = yield* Effect.match(
          control.run({
            _tag: "Plan",
            planId: rejected.planId,
            digest: rejected.digest,
            envelope: rejected.envelope,
            idempotencyKey: "run:denied" as ControlSchema.IdempotencyKey
          }),
          { onFailure: (error) => String(error._tag), onSuccess: () => "launched" }
        )

        // -------------------------------------------------------- the run gates
        // A real durable run that parks. `execute` returns the moment the park
        // is durable; nothing is driving the run after that.
        //
        // Drive one reaches the clearance step, which registers its own token,
        // finds it undecided, and parks the run under `approval`.
        yield* Ship.execute({ build: "v1.4.0" }, { executionId: shipRunId, discard: true })
        const first = yield* control.list({ _tag: "runs", filters: { runId: shipRunId } })
        const firstRow = first._tag === "runs" ? first.items[0] : undefined

        // The decision, taken from outside the run against the token the STEP
        // registered. `approve` looks the token up rather than creating one, so
        // this call could not have run before the step parked.
        const request = clearanceRequest(shipRunId)
        const approvalReceipt = yield* control.approve({
          target: request,
          scope: "once",
          idempotencyKey: "approve:node" as ControlSchema.IdempotencyKey
        })
        // Read the actual answer. A lookup/storage failure is not approval.
        const token = yield* runtime.registerApproval(request)

        // Drive two: the same step runs again, reads the resolved token, and
        // lets the run through to its next wait instead of asking a second
        // time.
        yield* Ship.execute({ build: "v1.4.0" }, { executionId: shipRunId, discard: true })
        const listed = yield* control.list({ _tag: "runs", filters: { runId: shipRunId } })
        const row = listed._tag === "runs" ? listed.items[0] : undefined

        // The signal. It records a fact and resumes nothing.
        yield* control.signal({
          runId: shipRunId as ControlSchema.RunId,
          signal: { name: "ship", payload: { approved: true, by: "release-manager" } },
          idempotencyKey: "signal:ship" as ControlSchema.IdempotencyKey
        })
        const signals = yield* runtime.deliveredSignals(shipRunId as ControlSchema.RunId)
        // The host turns the recorded fact into a completed wait point.
        const delivered = yield* deliverSignals(shipRunId)
        const result = yield* Ship.execute({ build: "v1.4.0" }, { executionId: shipRunId })
        const approvedReads = [...clearanceReads]

        // Denial uses the exact same action implementation and must fail
        // before the following signal wait can be activated.
        const deniedRunId = `${shipRunId}-denied`
        yield* Ship.execute({ build: "v1.4.1" }, { executionId: deniedRunId, discard: true })
        yield* control.deny({
          target: clearanceRequest(deniedRunId),
          scope: "once",
          idempotencyKey: "deny:node"
        })
        const deniedFailure = yield* Effect.flip(Ship.execute({ build: "v1.4.1" }, { executionId: deniedRunId }))

        const journal = yield* Journal.Journal
        yield* journal.flush
        const page = yield* journal.entries({ runId: shipRunId as JournalEvent.RunId, limit: 500 })
        const approvals = page.entries
          .filter((entry) => entry.eventType === "control.approval.approved")
          .map((entry) => (entry.payload as { readonly target: string }).target)

        return {
          plan: {
            beforeApproval: receipt(beforeApproval),
            beforeApprovalStatus: "status" in beforeApproval ? String(beforeApproval.status) : "none",
            afterApproval: receipt(afterApproval),
            decision: approved.decision,
            deniedDecision: deniedPlan.decision,
            deniedLaunch,
            launches
          },
          run: {
            firstPark: firstRow?.status ?? "unknown",
            firstWaitingFor: firstRow?.waitingReason,
            clearanceReads: approvedReads,
            parked: row?.status ?? "unknown",
            waitingFor: row?.waitingReason,
            approvalReceipt: receipt(approvalReceipt),
            approvalDecision: token._tag,
            deniedClearanceReads: clearanceReads.slice(approvedReads.length),
            deniedFailure: deniedFailure._tag,
            approvals,
            signals: signals.map((signal) => signal.name),
            delivered,
            result
          }
        } satisfies Summary
      }).pipe(Effect.provide(stack))
    )
  }).pipe(Effect.orDie)
```

---

# Cancel and child cleanup
https://smithers.sh/docs/examples/19-cancel-and-child-cleanup/

Cancel a run and observe attached child and process cleanup.

The parent's terminal transaction records cancellation requests for linked descendants under their recorded parent-exit policy. Each child's driver settles its own request under ownership fencing.

For operating-system processes, `NodeRuntime.layerHost` tracks process groups in a durable ledger and terminates the group when its action scope closes. The example checks both durable child state and process cleanup.

## Run the test

The complete program is [`examples/src/19-cancel-and-child-cleanup.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/19-cancel-and-child-cleanup.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/19-cancel-and-child-cleanup.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/run-store`](/docs/reference/api/run-store/)

## Source

```ts title="examples/src/19-cancel-and-child-cleanup.ts"
import { Capability } from "@smthrs/flows"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import { Action, Flow, Interpreter, WaitFor } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as DurableEngineState from "@smthrs/engine-store/DurableEngineState"
import { RunStore } from "@smthrs/run-store"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { dirname } from "node:path"

/** The child's one step: park until somebody answers. */
export const Watch = Flow.make("examples/Watch", {
  payload: { target: Schema.String },
  success: Schema.Json,
  error: WaitFor.WaitForRequestInvalid,
  body: () => WaitFor.action.call({ name: "clearance" })
})

/**
 * The parent. `.child()` makes the call a run of its own, and waiting for its
 * result is what records `onParentExit: "cancel"` on it. An attached child
 * exists because something is waiting for it.
 */
export const Deploy = Flow.make("examples/Deploy", {
  payload: { target: Schema.String },
  success: Schema.Json,
  error: WaitFor.WaitForRequestInvalid,
  body: (payload: { readonly target: string }) => Watch.child(payload)
})

/** The parent's execution id. Its child derives one from it. */
export const deployRunId = "deploy-1"

/** One run row, as durable state records it after the cancellation. */
export interface RunState {
  readonly runId: string
  readonly status: string
  readonly cancelRequested: boolean
}

/** What the cascade did. */
export interface CascadeSummary {
  /** The parent and the child before anything was cancelled. */
  readonly before: ReadonlyArray<RunState>
  /** The same two runs after the parent was interrupted. */
  readonly after: ReadonlyArray<RunState>
  /** The runs the interruption cascaded to, as the parent journalled them. */
  readonly cascadedTo: ReadonlyArray<string>
  /** The child spawns the parent recorded as effect boundaries. */
  readonly spawned: ReadonlyArray<string>
}

/** What the containment half observed. */
export interface ContainmentSummary {
  /** The process group the cancelled step started. */
  readonly pgid: number
  /** Whether that group was alive while the step was running. */
  readonly aliveDuringRun: boolean
  /** Whether it survived the cancellation, which it must not. */
  readonly survivedCancel: boolean
}

const stateOf = (row: {
  readonly runId: string
  readonly status: string
  readonly cancelRequestedAtMs: number | null
}): RunState => ({
  runId: row.runId,
  status: row.status,
  cancelRequested: row.cancelRequestedAtMs !== null
})

/** The engine, on the real Node host, with permission to spawn. */
const host = <Registered, RegistrationRequirements>(
  filename: string,
  hostId: string,
  registrations: Layer.Layer<Registered, never, RegistrationRequirements>
) =>
  NodeRuntime.layerHost(
    {
      filename,
      workspaceRoot: dirname(filename),
      owner: { hostId },
      // No signal handlers: the example is the one deciding when things stop.
      signals: [],
      rules: [
        new Capability.Permission.Rule({
          effect: "allow",
          pattern: new Capability.Capability.CapabilityPattern({ action: "proc:spawn", resource: "*" })
        })
      ]
    },
    registrations
  )

/** Waits until `read` answers `true`, or gives up after `budgetMs`. */
const until = (
  read: Effect.Effect<boolean>,
  budgetMs: number
): Effect.Effect<boolean> =>
  Effect.gen(function*() {
    const deadline = Date.now() + budgetMs
    while (Date.now() < deadline) {
      if (yield* read) return true
      yield* Effect.sleep("20 millis")
    }
    return yield* read
  })

/**
 * Starts a parent whose child is parked, cancels the parent, and reads both run
 * rows back out of durable state.
 */
export const cascade = (filename: string): Effect.Effect<CascadeSummary> =>
  Effect.gen(function*() {
    const registrations = Layer.mergeAll(
      WaitFor.layer,
      Interpreter.layer(Deploy),
      Interpreter.layer(Watch)
    ).pipe(Layer.provideMerge(Action.layerImplementations))

    return yield* Effect.scoped(
      Effect.gen(function*() {
        const runs = yield* RunStore.RunStore
        const state = yield* DurableEngineState.DurableEngineState

        // The parent starts, the child starts, and the child parks. `execute`
        // returns while both are still open, which is the state a cancellation
        // has to be able to reach.
        yield* Deploy.execute({ target: "server" }, { executionId: deployRunId, discard: true })
        const edges = yield* until(
          Effect.map(state.runChildren(deployRunId), (found) => found.length > 0),
          5_000
        ).pipe(Effect.andThen(state.runChildren(deployRunId)))
        const childId = edges[0]?.childId ?? "no-child"
        const before = yield* Effect.forEach([deployRunId, childId], (runId) =>
          Effect.map(runs.get(runId), stateOf))

        // The cancellation. `interrupt` is the durable path: the terminal
        // transition and the descendants' cancel requests commit together.
        yield* Deploy.interrupt(deployRunId)
        yield* until(
          Effect.map(runs.get(deployRunId), (row) => row.status === "cancelled").pipe(Effect.orDie),
          5_000
        )
        yield* until(
          Effect.map(
            runs.get(childId),
            (row) => row.cancelRequestedAtMs !== null || row.status === "cancelled"
          ).pipe(Effect.orDie),
          5_000
        )
        const after = yield* Effect.forEach([deployRunId, childId], (runId) =>
          Effect.map(runs.get(runId), stateOf))

        // The parent journalled what it decided about each linked child.
        const journal = yield* Journal.Journal
        yield* journal.flush
        const page = yield* journal.entries({ runId: deployRunId as JournalEvent.RunId, limit: 500 })
        // The interruption record names every run the cascade reached. It is
        // written in the same transaction as the parent's terminal row, which
        // is what makes "cancelled parent, live child" unrepresentable.
        const cascadedTo = page.entries
          .filter((entry) => entry.eventType === "flows.engine.interrupted")
          .flatMap((entry) => (entry.payload as { readonly cascadedTo?: ReadonlyArray<string> }).cascadedTo ?? [])
        // The spawn itself is a recorded effect boundary, so the lineage is
        // auditable rather than inferred from the edge table alone.
        const spawned = page.entries
          .filter((entry) => entry.eventType === "flows.time-travel.effect-boundary")
          .map((entry) => (entry.payload as { readonly effect?: { readonly kind?: string } }).effect?.kind)
          .filter((kind): kind is string => kind !== undefined)

        return { before, after, cascadedTo, spawned } satisfies CascadeSummary
      }).pipe(Effect.provide(host(filename, "examples-cancel", registrations)))
    )
  }).pipe(Effect.orDie)

/** Whether a process group still exists. */
const groupIsAlive = (pgid: number): boolean => {
  try {
    process.kill(-pgid, 0)
    return true
  } catch {
    return false
  }
}

/** The step that opens a process group and then waits for it. */
export const Hold = Action.make("examples/HoldProcess", {
  payload: { seconds: Schema.Number },
  success: Schema.String
})

/** The flow that holds a real process group open while it is cancelled. */
export const Occupy = Flow.make("examples/Occupy", {
  payload: { seconds: Schema.Number },
  success: Schema.String,
  body: (payload: { readonly seconds: number }) => Hold.call(payload)
})

/** The occupying run's execution id. */
export const occupyRunId = "occupy-1"

/**
 * Starts a run whose step holds a real process group, cancels it, and checks
 * that the group is gone.
 */
export const contained = (filename: string): Effect.Effect<ContainmentSummary> =>
  Effect.gen(function*() {
    let pgid = 0
    let started = false

    const hold = Hold.toLayer(({ seconds }) =>
      Effect.gen(function*() {
        const spawner = yield* ChildProcessSpawner
        // Two processes so the group outlives anything short of a group signal,
        // which is exactly what an unconstrained kill would miss.
        const handle = yield* Effect.orDie(
          spawner.spawn(ChildProcess.make("sh", ["-c", `sleep ${seconds} & sleep ${seconds}`]))
        )
        pgid = handle.pid as number
        started = true
        yield* Effect.orDie(handle.exitCode)
        return "finished"
      })
    )

    const registrations = Layer.mergeAll(hold, Interpreter.layer(Occupy)).pipe(
      Layer.provideMerge(Action.layerImplementations)
    )

    const aliveDuringRun = yield* Effect.scoped(
      Effect.gen(function*() {
        // The run holds the group open, so it is started in the background and
        // cancelled while its step is still running.
        const running = yield* Effect.forkChild(
          Occupy.execute({ seconds: 300 }, { executionId: occupyRunId })
        )
        yield* until(Effect.sync(() => started && groupIsAlive(pgid)), 10_000)
        const alive = groupIsAlive(pgid)
        yield* Occupy.interrupt(occupyRunId)
        yield* Fiber.await(running)
        return alive
      }).pipe(Effect.provide(host(filename, "examples-contained", registrations)))
    )

    // The scope is closed: the spawner's finalizer has signalled and killed the
    // group, and the reaper had nothing left to do.
    const survivedCancel = yield* until(Effect.sync(() => !groupIsAlive(pgid)), 10_000).pipe(
      Effect.map((gone) => !gone)
    )

    return { pgid, aliveDuringRun, survivedCancel } satisfies ContainmentSummary
  }).pipe(Effect.orDie)
```

---

# Child flows
https://smithers.sh/docs/examples/20-child-flows/

Run two child flows, join their results, and resume the parent without duplicating the children.

`flow.child` creates a separate durable execution whose identity is derived from its parent and node address. `flow.call` instead composes into the caller's graph. Re-driving the parent reuses the children's recorded results.

The final scenario exposes a flow as an agent tool. That handler chooses an explicit execution ID so repeated calls refer to the intended child work.

## Run the test

The complete program is [`examples/src/20-child-flows.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/20-child-flows.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/20-child-flows.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@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/model`](/docs/reference/api/model/) · [`@smthrs/plan`](/docs/reference/api/plan/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/)

## Source

```ts title="examples/src/20-child-flows.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import * as Effects from "@smthrs/core/Effects"
import * as CoreFlow from "@smthrs/core/Flow"
import * as DurableEngineState from "@smthrs/engine-store/DurableEngineState"
import { Action, Flow, type FlowRuntime, Interpreter } from "@smthrs/flow"
import * as FlowBinding from "@smthrs/harness/FlowBinding"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import { Node } from "@smthrs/plan"
import type * as Planned from "@smthrs/plan/Planned"
import * as Registry from "@smthrs/registry/Registry"
import { RunStore } from "@smthrs/run-store"
import type * as Context from "effect/Context"
import type * as Crypto from "effect/Crypto"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { durableEngine } from "./durable-layer.ts"

/** The work the first child does. */
export const Bundle = Action.make("examples/Bundle", {
  payload: { target: Schema.String },
  success: Schema.String
})

/** The work the second child does. */
export const Sign = Action.make("examples/Sign", {
  payload: { target: Schema.String },
  success: Schema.String
})

/** The fan-in step: both children's results arrive as payload fields. */
export const Report = Action.make("examples/Report", {
  payload: { bundle: Schema.String, signature: Schema.String },
  success: Schema.String
})

/** The first child, an ordinary flow. Nothing marks it as a child. */
export const Compile = Flow.make("examples/Compile", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload: { readonly target: string }) => Bundle.call(payload)
})

/** The second child. */
export const Notarize = Flow.make("examples/Notarize", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload: { readonly target: string }) => Sign.call(payload)
})

/**
 * The parent. `.child()` is the only difference from an inline call, and it is
 * the difference between one run and three.
 */
export const Release = Flow.make("examples/Release", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: ({ target }: { readonly target: string }) =>
    Node.bindPlanned(
      Node.all({
        bundle: Compile.child({ target }),
        signature: Notarize.child({ target })
      }),
      (results: Planned.Planned<{ readonly bundle: string; readonly signature: string }>) =>
        Report.call({ bundle: results.bundle, signature: results.signature })
    )
})

/**
 * `examples/Compile`, declared as something a model may call.
 *
 * A tool is a declaration plus the code that runs it, and the code here is one
 * line: execute the flow. The model never learns that `compile` is a whole
 * durable flow rather than a function, which is the point of the binding
 * contract.
 */
export const CompileTool = CoreFlow.make({
  name: "compile",
  description: "Compile a target and answer with the bundle path.",
  input: Schema.Struct({ target: Schema.String }),
  output: Schema.Struct({ bundle: Schema.String }),
  effects: Effects.make({ reads: [], writes: [], mode: "expected", onConflict: "serialize" })
})

/**
 * The execution id the tool's handler runs `examples/Compile` under.
 *
 * A constant, because the handler owns this decision. A `.child()` boundary
 * derives the id from the parent and the node address; a tool call has no
 * boundary to derive it from, so a handler that wants at-most-once has to name
 * an id that is stable for the same work.
 */
export const toolRunId = "compile-by-tool"

/**
 * The tool source a host binds so a cell can reach the flow.
 *
 * The handler needs whatever the flow needs: the engine, the crypto the
 * execution id is derived with, and the action implementations the body calls.
 * The host hands it exactly that context and nothing else.
 */
export const compileSource = (
  services: Context.Context<
    FlowRuntime.FlowRuntime | Crypto.Crypto | Action.Requirement<"examples/Bundle">
  >
): FlowBinding.Source =>
  FlowBinding.source("release/tools", [
    FlowBinding.provide(
      FlowBinding.make({
        flow: CompileTool,
        handler: ({ target }) =>
          Effect.map(
            Compile.execute({ target }, { executionId: toolRunId }),
            (bundle) => ({ bundle })
          )
      }),
      services
    )
  ])

/** What the model must answer with once it has used the tool. */
export const Built = Schema.Struct({ bundle: Schema.String })

/** The step that reaches the flow as a tool. */
export const Builder = AgentAction.make("examples/Builder", {
  payload: { target: Schema.String },
  output: Built,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You build releases. Use the compile tool rather than guessing at a path."],
  prompt: ({ target }) => `Compile the target and report the bundle path.\nTARGET: ${target}`
})

/** The flow the agent step runs inside. */
export const Build = Flow.make("examples/Build", {
  payload: { target: Schema.String },
  success: Built,
  error: AgentAction.AgentFailure,
  body: (payload: { readonly target: string }) => Builder.call(payload)
})

const prepared: Route.PreparedRequest = {
  routeId: "examples",
  protocolId: "examples",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

/** A scripted model that reads the target out of its prompt and calls the tool. */
export const scripted = (): Model.Model =>
  Model.make({
    stream: (request) =>
      Stream.suspend(() => {
        const text = [
          ...request.system.map((part) => part.text),
          ...request.messages.flatMap((message) =>
            message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
          )
        ].join("\n")
        const target = /TARGET: (.+)/.exec(text)?.[1]?.trim() ?? ""
        const cell = [
          `const built = await ctx.call("compile", { target: ${JSON.stringify(target)} })`,
          "ctx.done({ bundle: built.bundle })"
        ].join("\n")
        return Stream.fromIterable([
          ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
          ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
          ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
          ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
        ])
      })
  })

/** One child run, as durable state records it. */
export interface Child {
  readonly runId: string
  /**
   * The parent the durable edge names.
   *
   * Read from `flows_run_parents` rather than from `RunRow.parentRunId`: that
   * column carries a trampoline's previous round, and a child's link to its
   * parent is the edge table plus the `parentExecutionId` the child's own
   * state document records. `EngineChildren` reads the same two.
   */
  readonly parentId: string
  /** The parent the child's own state document records. */
  readonly parentExecutionId: string | undefined
  readonly status: string
}

/** What the two executions of the parent observed. */
export interface Summary {
  /** The report the first execution produced. */
  readonly report: string
  /** The report the re-driven execution produced. */
  readonly replayed: string
  /** The children the engine linked to the parent, in the order it linked them. */
  readonly children: ReadonlyArray<Child>
  /**
   * How many times each child's body ran, across both executions of the parent
   * and the tool call that compiles a second time under its own run.
   */
  readonly dispatches: Readonly<Record<string, number>>
  /** The bundle path the model reported after calling the flow as a tool. */
  readonly built: string
  /** The id of the run the tool call opened. */
  readonly toolRunId: string
  /** The status of the run the tool call opened. */
  readonly toolRunStatus: string
  /** The parents the durable edge table names for that run. */
  readonly toolRunParents: ReadonlyArray<string>
  /** The run the agent step itself executed as. */
  readonly builderRunId: string
}

/** The parent's execution id. Its children derive theirs from it. */
export const releaseRunId = "release-1"

/** The execution id the agent step that calls the tool runs under. */
export const builderRunId = "build-1"

/** Runs the parent twice over one SQLite file and reads its lineage back. */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const dispatches: Record<string, number> = { bundle: 0, sign: 0, report: 0 }

    const bundle = Bundle.toLayer(({ target }) =>
      Effect.sync(() => {
        dispatches.bundle! += 1
        return `dist/${target}.js`
      })
    )
    const sign = Sign.toLayer(({ target }) =>
      Effect.sync(() => {
        dispatches.sign! += 1
        return `${target}.sig`
      })
    )
    const report = Report.toLayer((parts) =>
      Effect.sync(() => {
        dispatches.report! += 1
        return `${parts.bundle} + ${parts.signature}`
      })
    )

    const stack = Layer.mergeAll(
      bundle,
      sign,
      report,
      Interpreter.layer(Release),
      Interpreter.layer(Compile),
      Interpreter.layer(Notarize)
    ).pipe(
      Layer.provideMerge(Action.layerImplementations),
      Layer.provideMerge(durableEngine(filename, "examples-release"))
    )

    return yield* Effect.scoped(
      Effect.gen(function*() {
        const first = yield* Release.execute({ target: "server" }, { executionId: releaseRunId })
        // The same execution id: a re-drive, not a second release. The children
        // are read out of durable state rather than started again.
        const replayed = yield* Release.execute({ target: "server" }, { executionId: releaseRunId })

        const state = yield* DurableEngineState.DurableEngineState
        const runs = yield* RunStore.RunStore
        const edges = yield* state.runChildren(releaseRunId)
        const children = yield* Effect.forEach(edges, (edge) =>
          Effect.map(runs.get(edge.childId), (row) => {
            const state = JSON.parse(row.stateJson) as { readonly parentExecutionId?: string }
            return {
              runId: row.runId,
              parentId: edge.parentId,
              parentExecutionId: state.parentExecutionId,
              status: row.status
            } satisfies Child
          }))

        // ------------------------------------- the same flow, as a model's tool
        // The context the tool handler runs the flow in: the engine, the
        // crypto, and the one action the flow's body calls.
        const services = yield* Effect.context<
          FlowRuntime.FlowRuntime | Crypto.Crypto | Action.Requirement<"examples/Bundle">
        >()
        const built = yield* Build.execute({ target: "server" }, { executionId: builderRunId }).pipe(
          Effect.provide(
            Layer.mergeAll(Builder.layer, Interpreter.layer(Build)).pipe(
              Layer.provideMerge(
                Layer.mergeAll(
                  AgentAction.layerHost({
                    registry: Registry.makeNoop({
                      list: () => Effect.succeed([]),
                      visible: () => Effect.succeed([]),
                      getOption: () => Effect.succeed(Option.none())
                    }),
                    flows: [compileSource(services)],
                    limits: { calls: 4 },
                    capabilityEnvelope: [],
                    maxFrames: 2
                  }),
                  SeatResolver.layer({
                    resolve: (id) =>
                      Effect.succeed(
                        Seat.make({
                          id,
                          model: scripted(),
                          route: { prepare: () => Effect.succeed(prepared) },
                          contextWindowTokens: 200_000
                        })
                      )
                  }),
                  Agent.layer
                )
              ),
              // The fixture model cannot report provider quota, and this
              // offline example has no approved envelope to turn into a cap.
              // eslint-disable-next-line no-restricted-syntax -- this offline example has no approved envelope
              Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerUnclassified(), Budget.layerUnbounded())),
              Layer.provideMerge(Agent.layerDefaults)
            )
          )
        )

        // The tool call opened a run of its own, linked to the run the step
        // was executing in.
        const toolRun = yield* runs.get(toolRunId)
        const toolParents = yield* state.runParents(toolRunId)

        return {
          report: first,
          replayed,
          children,
          dispatches,
          built: built.bundle,
          toolRunId: toolRun.runId,
          toolRunStatus: toolRun.status,
          toolRunParents: toolParents.map((edge) => edge.parentId),
          builderRunId
        } satisfies Summary
      }).pipe(Effect.provide(stack))
    )
  }).pipe(Effect.provide(NodeCrypto.layer), Effect.orDie)
```

---

# Cache and compensation
https://smithers.sh/docs/examples/21-cache-and-compensation/

Apply a cache lifetime and restore workspace state before retrying a compensable action.

`CacheEnvironment.withCache` declares the result's scope and age bound. The engine records its cache-age decision so a resumed execution can reuse that decision.

The compensable action captures a workspace pre-image and restores it before another attempt. The example uses a directory-copying `Jj` implementation to check the file state. Cross-run sealed reuse also needs an idempotency key, a hard file boundary, and a complete cache environment.

## Run the test

The complete program is [`examples/src/21-cache-and-compensation.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/21-cache-and-compensation.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/21-cache-and-compensation.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/)

## Source

```ts title="examples/src/21-cache-and-compensation.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import { StepBoundary, WorkspaceSandbox } from "@smthrs/engine-store"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as CacheEnvironment from "@smthrs/flow/CacheEnvironment"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import { Journal, type JournalEvent } from "@smthrs/journal"
import { Jj } from "@smthrs/kernel"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { dirname, join } from "node:path"
import { stubJj } from "./durable-layer.ts"

/** The failure the first attempt of the compensable step raises. */
export class Interrupted extends Schema.TaggedError<Interrupted>()("examples/Interrupted", {
  attempt: Schema.Number
}) {}

/**
 * What a snapshot the engine asked for is FOR, read off the message it passes.
 *
 * The engine takes two kinds of pre-image around a compensable dispatch and one
 * post-image after it, and each message names the step key and the attempt. The
 * digests are not stable across edits, so the example reports the readable
 * half, which attempt and whether the snapshot was taken before or after it,
 * and leaves the key out.
 */
const describe = (message: string | undefined): string => {
  const matched = /attempt (\d+)( settled)?$/.exec(message ?? "")
  if (matched === null) return message ?? "unknown"
  return `attempt-${matched[1]}-${matched[2] === undefined ? "pre" : "post"}`
}

/**
 * A version-control service that snapshots a directory by copying it.
 *
 * `stubJj` in `durable-layer.ts` records nothing, which is honest for examples
 * whose actions are sealed. A compensable action needs a real one: the engine
 * calls `snapshot` before each attempt and `restore` before a retry, and if
 * neither does anything then nothing is compensated. This one is the smallest
 * implementation that genuinely puts files back.
 */
export const directoryJj = (options: {
  readonly workspace: string
  readonly snapshots: string
  readonly log: { readonly snapshots: Array<string>; readonly restores: Array<string> }
}) => {
  let counter = 0
  const taken = new Map<string, string>()
  return Layer.succeed(
    Jj.Jj,
    Jj.make({
      snapshot: (message) =>
        Effect.sync(() => {
          counter += 1
          const changeId = `snapshot-${counter}`
          const destination = join(options.snapshots, changeId)
          rmSync(destination, { recursive: true, force: true })
          cpSync(options.workspace, destination, { recursive: true })
          taken.set(changeId, describe(message))
          options.log.snapshots.push(describe(message))
          return { changeId: changeId as never }
        }),
      restore: (changeId) =>
        Effect.sync(() => {
          options.log.restores.push(taken.get(changeId) ?? changeId)
          rmSync(options.workspace, { recursive: true, force: true })
          cpSync(join(options.snapshots, changeId), options.workspace, { recursive: true })
        }),
      diff: () => Effect.succeed(""),
      workspaceAdd: () => Effect.void,
      workspaceForget: () => Effect.void,
      status: () => Effect.succeed("")
    })
  )
}

/**
 * The engine `durable-layer.ts` builds, plus the two things this example needs
 * it to have: a real `Jj`, and the complete cache environment a cross-run key
 * is derived from.
 *
 * Both are provided BENEATH the engine, where the dispatch reads them, rather
 * than beside the caller.
 */
const engine = (filename: string, hostId: string, jj: Layer.Layer<Jj.Jj>) =>
  NodeRuntime.layer(
    { filename, workspaceRoot: dirname(filename), owner: { hostId }, isAlive: () => Effect.succeed(false) },
    StepBoundary.layer,
    WorkspaceSandbox.layerFileSystem(),
    Layer.empty
  ).pipe(
    Layer.provideMerge(
      Layer.mergeAll(jj, Action.layerCacheEnvironment({ layers: [], capabilities: {} }))
    ),
    Layer.provideMerge(NodeCrypto.layer),
    Layer.provideMerge(NodeFileSystem.layer)
  )

/** The declared atom the caching flow's body names. */
export const Build = Action.make("examples/CachedBuild", {
  payload: { target: Schema.String },
  success: Schema.String
})

/** The flow that runs one cached build step. */
export const Package = Flow.make("examples/Package", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload: { readonly target: string }) => Build.call(payload)
})

/** The declared atom the compensating flow's body names. */
export const Apply = Action.make("examples/ApplyMigration", {
  payload: { migration: Schema.String },
  success: Schema.String,
  // The retry ladder can still run out, so the failure stays declared rather
  // than swallowed. It does not happen here, because attempt two succeeds, and
  // a caller that hid it would turn an exhausted migration into a silent one.
  error: Interrupted
})

/** The flow that runs one compensable migration step. */
export const Migrate = Flow.make("examples/Migrate", {
  payload: { migration: Schema.String },
  success: Schema.String,
  error: Interrupted,
  body: (payload: { readonly migration: string }) => Apply.call(payload)
})

/** What one cache scenario observed. */
export interface CacheSummary {
  /** What each of the two runs answered. */
  readonly results: readonly [string, string]
  /** How many times the sealed body executed across both runs. */
  readonly executions: number
  /** Every time-to-live verdict the runs journalled, in order. */
  readonly verdicts: ReadonlyArray<string>
}

/** What the compensation scenario observed. */
export interface CompensationSummary {
  /** The value the run settled with. */
  readonly result: string
  /** The attempt numbers the body saw. */
  readonly attempts: ReadonlyArray<number>
  /** The workspace file after the run, which is the evidence. */
  readonly workspace: string
  /**
   * The snapshots the engine took, in order.
   *
   * Two pre-images per attempt and one post-image after it. The pair is not a
   * duplicate: one is the rollback boundary around the whole compensable
   * dispatch, the other is the attempt row's own pre-image, which is what an
   * adopted attempt restores after a crash. The post-image is what the boundary
   * diffs the pre-image against.
   */
  readonly snapshots: ReadonlyArray<string>
  /** The pre-images the engine put back before the retry. */
  readonly restores: ReadonlyArray<string>
}

const ttlVerdicts = (
  runId: string
): Effect.Effect<ReadonlyArray<string>, never, Journal.Journal> =>
  Effect.gen(function*() {
    const journal = yield* Journal.Journal
    yield* journal.flush
    const page = yield* journal.entries({ runId: runId as JournalEvent.RunId, limit: 500 })
    return page.entries
      .filter((entry) => entry.eventType === "flows.engine.cache-provenance")
      .map((entry) => entry.payload as { readonly action?: string; readonly verdict?: string })
      .filter((payload) => payload.action === "ttl" && typeof payload.verdict === "string")
      .map((payload) => payload.verdict!)
  }).pipe(Effect.orDie)

/**
 * Runs the same sealed step in two separate runs under one cache policy.
 *
 * With a generous `ttlMs` the second run reads the recorded row and journals
 * `admitted`. With `ttlMs: 1` and a pause between the runs the row has aged
 * out, so the second run journals `expired` and executes the body again.
 */
export const cached = (
  filename: string,
  options: { readonly ttlMs: number; readonly pauseMs: number; readonly prefix: string }
): Effect.Effect<CacheSummary> =>
  Effect.gen(function*() {
    let executions = 0

    /**
     * The sealed step whose result is addressable across runs. `idempotencyKey`
     * gives it an identity a second run can derive, the hard boundary declares
     * it hermetic, and the policy states how long the row stays good.
     */
    const compile = CacheEnvironment.withCache(
      Action.make({
        name: "examples/CachedCompile",
        success: Schema.String,
        tier: "sealed",
        idempotencyKey: "examples/cached-compile/v1",
        metadata: { readSet: [], writeSet: [], boundaryMode: "hard" },
        execute: Effect.sync(() => {
          executions += 1
          return "dist/server.js"
        })
      }),
      { ttlMs: options.ttlMs, scope: "shared" }
    )

    const stack = Layer.mergeAll(
      Build.toLayer(({ target }) => Effect.map(compile, (artifact) => `${artifact}?target=${target}`)),
      Interpreter.layer(Package)
    ).pipe(
      Layer.provideMerge(Action.layerImplementations),
      // A sealed step never asks for a pre-image, so the cache half runs on
      // `durable-layer.ts`'s recording-nothing stub. Only the compensable half
      // below needs a version-control service that really moves files.
      Layer.provideMerge(engine(filename, "examples-cache", stubJj))
    )

    return yield* Effect.scoped(
      Effect.gen(function*() {
        const first = yield* Package.execute({ target: "server" }, { executionId: `${options.prefix}-1` })
        if (options.pauseMs > 0) yield* Effect.sleep(`${options.pauseMs} millis`)
        const second = yield* Package.execute({ target: "server" }, { executionId: `${options.prefix}-2` })
        const verdicts = [
          ...yield* ttlVerdicts(`${options.prefix}-1`),
          ...yield* ttlVerdicts(`${options.prefix}-2`)
        ]
        return { results: [first, second] as const, executions, verdicts } satisfies CacheSummary
      }).pipe(Effect.provide(stack))
    )
  }).pipe(Effect.orDie)

/**
 * Runs a compensable step that fails halfway through its first attempt.
 *
 * The body appends to a real file before it fails, so the pre-image is the only
 * thing standing between attempt two and a doubled migration.
 */
export const compensated = (
  filename: string,
  directories: { readonly workspace: string; readonly snapshots: string }
): Effect.Effect<CompensationSummary> =>
  Effect.gen(function*() {
    const log = { snapshots: [] as Array<string>, restores: [] as Array<string> }
    const attempts: Array<number> = []
    const ledger = join(directories.workspace, "schema.sql")

    yield* Effect.sync(() => {
      mkdirSync(directories.workspace, { recursive: true })
      mkdirSync(directories.snapshots, { recursive: true })
      writeFileSync(ledger, "-- base\n")
    })

    /**
     * A compensable step: it writes before it can fail, so the engine takes a
     * pre-image first and restores it before the retry.
     */
    const applyMigration = Action.make({
      name: "examples/ApplyMigrationStep",
      success: Schema.String,
      error: Interrupted,
      tier: "compensable",
      execute: Effect.gen(function*() {
        const attempt = yield* Action.CurrentAttempt
        attempts.push(attempt)
        yield* Effect.sync(() => {
          writeFileSync(ledger, `${readFileSync(ledger, "utf8")}ALTER TABLE runs ADD COLUMN lane;\n`)
        })
        // The write is already on disk. Failing here is what a half-applied
        // migration looks like, and it is what the pre-image undoes.
        if (attempt === 1) return yield* Effect.fail(new Interrupted({ attempt }))
        return "applied"
      })
    })

    const stack = Layer.mergeAll(
      Apply.toLayer(({ migration }) =>
        Action.retry(applyMigration, { times: 1 }).pipe(
          Effect.map((outcome) => `${migration}:${outcome}`)
        )
      ),
      Interpreter.layer(Migrate)
    ).pipe(
      Layer.provideMerge(Action.layerImplementations),
      Layer.provideMerge(engine(filename, "examples-compensate", directoryJj({ ...directories, log })))
    )

    const result = yield* Effect.scoped(
      Migrate.execute({ migration: "0007-lane" }, { executionId: "migrate-1" }).pipe(
        Effect.provide(stack)
      )
    )

    return {
      result,
      attempts,
      workspace: readFileSync(ledger, "utf8"),
      snapshots: log.snapshots,
      restores: log.restores
    } satisfies CompensationSummary
  }).pipe(Effect.orDie)
```

---

# Mcp server
https://smithers.sh/docs/examples/22-mcp-server/

The MCP server `22-mcp-tools.ts` connects to.

It is a separate process on purpose: an MCP session is a subprocess speaking newline-delimited JSON-RPC over stdio, and an in-process double would prove nothing about the transport, the handshake, or the framing. Nothing here imports from this repository, because the point of the protocol is that the server on the other end need not be ours.

Usage: `node 22-mcp-server.ts`. It answers `initialize`, `tools/list`, and `tools/call` and exits when its standard input closes.

## Run the test

The complete program is [`examples/src/22-mcp-server.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/22-mcp-server.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

This program has no dedicated test file. Read its setup requirements before running it; importing an example alone may only define its exports.

## Related reference

## Source

```ts title="examples/src/22-mcp-server.ts"
import { createInterface } from "node:readline"

const PROTOCOL_VERSION = "2025-06-18"

interface Request {
  readonly id?: number | string
  readonly method: string
  readonly params?: { readonly name?: string; readonly arguments?: Record<string, unknown> }
}

const tools = [
  {
    name: "word_count",
    description: "Counts the words in a piece of text",
    inputSchema: {
      type: "object",
      properties: { text: { type: "string" } },
      required: ["text"]
    }
  },
  {
    name: "slugify",
    description: "Turns a title into a url slug",
    inputSchema: {
      type: "object",
      properties: { text: { type: "string" } },
      required: ["text"]
    }
  },
  {
    name: "explode",
    description: "Always reports a tool-level failure",
    inputSchema: { type: "object", properties: {} }
  }
]

const send = (message: unknown): void => {
  process.stdout.write(`${JSON.stringify(message)}\n`)
}

const text = (value: string, isError = false) => ({
  content: [{ type: "text", text: value }],
  isError
})

const call = (name: string | undefined, args: Record<string, unknown>) => {
  const subject = typeof args.text === "string" ? args.text : ""
  if (name === "word_count") {
    return text(String(subject.split(/\s+/u).filter((word) => word.length > 0).length))
  }
  if (name === "slugify") {
    return text(subject.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, ""))
  }
  return text("the tool refused", true)
}

createInterface({ input: process.stdin }).on("line", (line: string) => {
  const request = JSON.parse(line) as Request
  if (request.method === "initialize") {
    send({
      jsonrpc: "2.0",
      id: request.id,
      result: {
        protocolVersion: PROTOCOL_VERSION,
        capabilities: { tools: {} },
        serverInfo: { name: "examples-fixture", version: "1.0.0" }
      }
    })
    return
  }
  if (request.method === "tools/list") {
    send({ jsonrpc: "2.0", id: request.id, result: { tools } })
    return
  }
  if (request.method === "tools/call") {
    send({
      jsonrpc: "2.0",
      id: request.id,
      result: call(request.params?.name, request.params?.arguments ?? {})
    })
    return
  }
  // A notification carries no id and expects no reply.
  if (request.id !== undefined) {
    send({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: `no method ${request.method}` } })
  }
})
```

---

# Mcp tools
https://smithers.sh/docs/examples/22-mcp-tools/

Give an agent tools supplied by a separate MCP server process.

`McpFlows.mcp` projects the server's catalog into callable flow bindings. The cell discovers and calls them through the same flow interface as other tools.

The example distinguishes a transport failure from a tool-reported `isError` result. The host explicitly grants the authority of the connected server; the adapter cannot infer the safety of opaque tool code. The companion `22-mcp-server.ts` implements the stdio protocol without importing Smithers.

## Run the test

The complete program is [`examples/src/22-mcp-tools.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/22-mcp-tools.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/22-mcp-tools.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/harness`](/docs/reference/api/harness/) · [`@smthrs/mcp`](/docs/reference/api/mcp/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/22-mcp-tools.ts"
import { NodeServices } from "@effect/platform-node"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import * as Capability from "@smthrs/capability/Capability"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as FlowBinding from "@smthrs/harness/FlowBinding"
import * as McpClient from "@smthrs/mcp/McpClient"
import * as McpFlows from "@smthrs/mcp/McpFlows"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Descriptor from "@smthrs/registry/Descriptor"
import * as Registry from "@smthrs/registry/Registry"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { durableEngine } from "./durable-layer.ts"

/** The name this session is known by. Every tool flow is scoped under it. */
export const serverName = "fixtures"

/** The fixture server's program file. */
export const serverProgram: string = join(dirname(fileURLToPath(import.meta.url)), "22-mcp-server.ts")

/** The capability this host is prepared to grant the fixture server's tools. */
export const grant = `proc:spawn:mcp/${serverName}`

/**
 * Re-declares a source's flows under the capabilities this host grants them.
 *
 * `McpFlows` declares every tool `"*"`, which is the repository's conservative
 * wildcard for authority a projector cannot describe. `MarkdownFlow` uses the
 * same spelling for a skill that declares none. The cell boundary checks a
 * declared capability as an EXACT `namespace:operation:resource` triple, so a
 * wildcard declaration parses as nothing and is refused by every envelope,
 * however wide. That refusal is the current behaviour and this example pins it.
 *
 * Narrowing here rather than widening the envelope is the right shape either
 * way: an envelope of `*:*` would grant the run everything, where this grants
 * exactly the tools of exactly this server.
 */
export const granting = (
  source: FlowBinding.Source,
  capabilities: ReadonlyArray<string>
): FlowBinding.Source => ({
  name: source.name,
  bindings: () =>
    Effect.map(source.bindings(), (bindings) =>
      bindings.map((binding) => ({
        descriptor: new Descriptor.FlowDescriptor({ ...binding.descriptor, capabilities }),
        run: binding.run
      })))
})

/** What the step must answer with. */
export const Reading = Schema.Struct({
  words: Schema.Number,
  slug: Schema.String,
  refused: Schema.Boolean
})

/** The step. Its prompt carries the title the tools are asked about. */
export const Describe = AgentAction.make("examples/DescribeTitle", {
  payload: { title: Schema.String },
  output: Reading,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You prepare release titles. Use the tools rather than counting by eye."],
  prompt: ({ title }) => `Count the words in the title and slugify it.\nTITLE: ${title}`
})

/** The flow that runs the one step reaching the MCP server's tools. */
export const Titles = Flow.make("examples/Titles", {
  payload: { title: Schema.String },
  success: Reading,
  error: AgentAction.AgentFailure,
  body: (payload: { readonly title: string }) => Describe.call(payload)
})

const prepared: Route.PreparedRequest = {
  routeId: "examples",
  protocolId: "examples",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

/**
 * A scripted model that reads the title out of the request and calls three MCP
 * tools by their scoped flow names.
 *
 * `mcp/<server>/<tool>` is the naming rule, and it is scoped by server so two
 * servers may offer a tool of the same name without colliding. The cell reports
 * the third call's `isError` rather than treating it as a failure, because that
 * is what it is.
 */
export const scripted = (asked: Array<string>): Model.Model =>
  Model.make({
    stream: (request) =>
      Stream.suspend(() => {
        const text = [
          ...request.system.map((part) => part.text),
          ...request.messages.flatMap((message) =>
            message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
          )
        ].join("\n")
        asked.push(text)
        const title = /TITLE: (.+)/.exec(text)?.[1]?.trim() ?? ""
        const cell = [
          `const counted = await ctx.call("mcp/${serverName}/word_count", { text: ${JSON.stringify(title)} })`,
          `const slugged = await ctx.call("mcp/${serverName}/slugify", { text: ${JSON.stringify(title)} })`,
          `const refused = await ctx.call("mcp/${serverName}/explode", {})`,
          "ctx.done({",
          "  words: Number(counted.content[0].text),",
          "  slug: slugged.content[0].text,",
          "  refused: refused.isError",
          "})"
        ].join("\n")
        return Stream.fromIterable([
          ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
          ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
          ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
          ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
        ])
      })
  })

/** A scripted model that reports what one call came back with, verbatim. */
const reporting = (flow: string): Model.Model =>
  Model.make({
    stream: () =>
      Stream.suspend(() => {
        const cell = [
          "let outcome",
          `try { outcome = { ok: true, value: await ctx.call(${JSON.stringify(flow)}, { text: "one two" }) } }`,
          "catch (error) { outcome = { ok: false, message: String(error && error.message ? error.message : error) } }",
          "ctx.done({ words: 0, slug: JSON.stringify(outcome), refused: !outcome.ok })"
        ].join("\n")
        return Stream.fromIterable([
          ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
          ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
          ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
          ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
        ])
      })
  })

/** What one run of the step observed. */
export interface Summary {
  /** The schema-typed answer the step settled with. */
  readonly reading: typeof Reading.Type
  /** The tools the server disclosed at connect time, in catalog order. */
  readonly tools: ReadonlyArray<string>
  /** The flow names the projected source offers the model. */
  readonly flowNames: ReadonlyArray<string>
  /** The capabilities the adapter declares, before the host narrows them. */
  readonly declaredCapabilities: ReadonlyArray<string>
  /** Whether the catalog reached the model's request. */
  readonly disclosed: boolean
  /** What a cell got back under an envelope that covers none of them. */
  readonly ungranted: string
}

/** Runs the step against a real MCP subprocess. */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const asked: Array<string> = []

    // Connecting owns a subprocess, so it is scoped: the session lives exactly
    // as long as the surrounding scope, and a host composes it once beside its
    // other scoped services.
    const client = yield* McpClient.connect({
      server: serverName,
      command: process.execPath,
      args: [serverProgram]
    })
    const projected = McpFlows.mcp(client)
    const declared = yield* projected.bindings()
    const source = granting(projected, [grant])
    const bindings = yield* source.bindings()

    const registry = Registry.makeNoop({
      list: () => Effect.succeed([]),
      visible: () => Effect.succeed([]),
      getOption: () => Effect.succeed(Option.none())
    })

    const run = (
      flows: ReadonlyArray<FlowBinding.Source>,
      model: Model.Model,
      envelope: ReadonlyArray<Capability.CapabilityPattern>,
      executionId: string
    ) =>
      Effect.scoped(
        Titles.execute({ title: "Durable Flows Release Notes" }, { executionId }).pipe(
          Effect.provide(
            Layer.mergeAll(Describe.layer, Interpreter.layer(Titles)).pipe(
              Layer.provideMerge(
                Layer.mergeAll(
                  AgentAction.layerHost({
                    registry,
                    flows,
                    limits: { calls: 8 },
                    capabilityEnvelope: envelope,
                    maxFrames: 2
                  }),
                  SeatResolver.layer({
                    resolve: (id) =>
                      Effect.succeed(
                        Seat.make({
                          id,
                          model,
                          route: { prepare: () => Effect.succeed(prepared) },
                          contextWindowTokens: 200_000
                        })
                      )
                  }),
                  Agent.layer
                )
              ),
              // The scripted seat has no provider quota to classify, and the
              // standalone example has no approved envelope to derive a cap.
              // eslint-disable-next-line no-restricted-syntax -- this offline example has no approved envelope
              Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerUnclassified(), Budget.layerUnbounded())),
              Layer.provideMerge(Agent.layerDefaults),
              Layer.provideMerge(Action.layerImplementations),
              Layer.provideMerge(durableEngine(filename, "examples-mcp"))
            )
          )
        )
      )

    // The envelope names the grant this host decided on, not `*:*`.
    const envelope = [new Capability.CapabilityPattern({ action: "proc:spawn", resource: `mcp/${serverName}` })]
    const reading = yield* run([source], scripted(asked), envelope, "titles-1")

    // The same tools under an envelope that grants something else entirely.
    // The adapter declares an MCP tool's authority as everything it could
    // possibly reach, one parseable `namespace:operation:resource` at a time,
    // so a host that granted only `net:get` admits none of it and the refusal
    // names the first capability the tool would have needed.
    const ungranted = yield* run(
      [projected],
      reporting(`mcp/${serverName}/word_count`),
      [new Capability.CapabilityPattern({ action: "net:get", resource: "example.invalid" })],
      "titles-ungranted"
    )

    return {
      reading,
      tools: client.tools.map((tool) => tool.name),
      flowNames: bindings.map((binding) => binding.descriptor.name),
      declaredCapabilities: [...new Set(declared.flatMap((binding) => binding.descriptor.capabilities))],
      disclosed: asked.every((text) => text.includes(`mcp/${serverName}/word_count`)),
      ungranted: ungranted.slug
    } satisfies Summary
  }).pipe(
    Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
    Effect.scoped,
    Effect.orDie
  )
```

---

# Control plane and gateway
https://smithers.sh/docs/examples/24-control-plane-and-gateway/

Plan, approve, launch, and watch a flow through a loopback control server.

Unary calls use HTTP RPC; the watch stream uses WebSocket RPC. The catalog comes from the example project's flow declarations, and its executor connects an approved plan to the registered durable implementation.

Control state and execution state use separate databases while the journal supplies the event stream. The server binds an ephemeral loopback port without authentication. A host listening beyond loopback needs an authenticator and client credentials.

## Run the test

The complete program is [`examples/src/24-control-plane-and-gateway.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/24-control-plane-and-gateway.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/24-control-plane-and-gateway.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/database`](/docs/reference/api/database/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/)

## Source

```ts title="examples/src/24-control-plane-and-gateway.ts"
import { NodeHttpClient, NodeHttpServer, NodeSocket } from "@effect/platform-node"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as NodePath from "@effect/platform-node/NodePath"
import { Control, ControlClient, ControlLive, ControlRuntime, SqlControlRuntime } from "@smthrs/control"
import * as ControlExecutor from "@smthrs/control/ControlExecutor"
import * as ControlRpcs from "@smthrs/control/ControlRpcs"
import type * as ControlSchema from "@smthrs/control/ControlSchema"
import * as ControlServer from "@smthrs/control/ControlServer"
import * as DurableWriter from "@smthrs/database/DurableWriter"
import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
import { Action, Flow, type FlowRuntime, Interpreter, WaitFor } from "@smthrs/flow"
import { NotificationQueue } from "@smthrs/notifications"
import { Executable, Registry } from "@smthrs/registry"
import { Migrations as RunStoreMigrations, RunStore } from "@smthrs/run-store"
import type * as Crypto from "effect/Crypto"
import * as Deferred from "effect/Deferred"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import type * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import type * as Path from "effect/Path"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { RpcSerialization } from "effect/unstable/rpc"
import { mkdirSync } from "node:fs"
import { createServer } from "node:http"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { durableEngine } from "./durable-layer.ts"

/**
 * The flow every descriptor in this project delegates to.
 *
 * Its payload is the `Executable.Invocation` envelope rather than a schema of
 * its own, because one registered delegate runs many descriptors: the envelope
 * carries the discovered flow's name, the caller's input, and the rendered
 * body. Here the body is a durable wait, so a launched run parks instead of
 * finishing, which is what gives the watch something to replay.
 */
export const Ship = Flow.make("examples/RemoteShip", {
  payload: Executable.Invocation,
  success: Schema.Json,
  error: WaitFor.WaitForRequestInvalid,
  body: () => WaitFor.action.call({ name: "clearance" })
})

/** The project whose `flows/` directory the control plane serves. */
export const projectRoot: string = join(dirname(fileURLToPath(import.meta.url)), "24-project")

/** The name discovery derives for `flows/ship/flow.mdx` from its directory. */
export const discoveredFlow = "ship"

/** The platform services discovery and body loading read the project through. */
const platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)

/**
 * The registry the control plane lists flows from and the engine registers
 * them from: `<projectRoot>/flows/**`, scanned, with bodies still unread.
 */
const registry = Executable.layerProject({ root: projectRoot }).pipe(Layer.provide(platform), Layer.orDie)

/** How a discovered descriptor is turned into something the engine can drive. */
const bridge: Executable.Options = { delegates: [Ship] }

/**
 * Starts a discovered flow on the durable engine.
 *
 * A bridged flow declares open requirements: the bridge cannot know at the
 * type level what the delegate a descriptor names will need, so `Executable`
 * says `any`. The host does know, because it registered the delegate, so the
 * launch is narrowed here instead of letting `any` widen every effect
 * downstream of it.
 */
const launch = (
  executable: Executable.Executable,
  input: Schema.Json,
  executionId: string
): Effect.Effect<string, never, FlowRuntime.FlowRuntime | Crypto.Crypto> =>
  (executable.flow.execute({ input }, { executionId, discard: true }) as Effect.Effect<
    string,
    unknown,
    FlowRuntime.FlowRuntime | Crypto.Crypto
  >).pipe(Effect.orDie)

/** The base URL of a listening TCP server. */
const addressOf = (server: HttpServer.HttpServer["Service"]): string => {
  const address = server.address
  if (address._tag !== "TcpAddress") throw new Error("expected a TCP control server")
  return `http://127.0.0.1:${address.port}`
}

/** What one remote session observed. */
export interface Summary {
  /** The base URL the server bound to, with its ephemeral port. */
  readonly url: string
  /** The flows `list` reported, discovered on disk and read over the wire. */
  readonly catalog: ReadonlyArray<string>
  /** The flow the plan card names, decoded on the client side of the wire. */
  readonly plannedFlow: string
  /** The collaborator flows the planned envelope carries, from the descriptor. */
  readonly plannedEnvelope: ReadonlyArray<string>
  /** The receipt `run` answered before the plan was approved. */
  readonly beforeApproval: string
  /** The receipt `run` answered after it was approved. */
  readonly afterApproval: string
  /** The run id that receipt named, which is the run the executor started. */
  readonly watchedRunId: string | undefined
  /** The flows the executor was handed, in launch order. */
  readonly launched: ReadonlyArray<string>
  /** The runs `list` reported, over the same connection. */
  readonly listed: ReadonlyArray<string>
  /** The status `list` reported for the approved plan's run. */
  readonly parked: string | undefined
  /** The control events the WebSocket watch replayed, oldest first. */
  readonly watched: ReadonlyArray<string>
}

/** The plane's status for an engine run that has stopped for now. */
const planeStatus = (status: RunStore.RunStatus): ControlSchema.RunStatus =>
  status === "suspended"
    ? "parked"
    : status === "completed" || status === "failed" || status === "cancelled" || status === "running"
    ? status
    : "accepted"

/**
 * Serves the control plane on loopback and drives it entirely through the RPC
 * client.
 *
 * @param root a directory the two SQLite files are created in
 */
export const main = (root: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const launched: Array<string> = []
    const driving: Array<{
      readonly runId: string
      readonly start: Deferred.Deferred<void>
      readonly fiber: Fiber.Fiber<unknown, unknown>
    }> = []

    yield* Effect.sync(() => mkdirSync(root, { recursive: true }))

    // What the plane may be asked to plan is what discovery found. The scan
    // happens once, here, so the runtime is configured with the descriptors on
    // disk rather than a list this file keeps in step with them by hand.
    const discovered = yield* Effect.gen(function*() {
      const found = yield* Registry.Registry
      return yield* found.list()
    }).pipe(Effect.provide(registry), Effect.orDie)

    const flows = discovered.map((descriptor): ControlRuntime.MemoryFlow => ({
      flowId: descriptor.name as ControlSchema.FlowId,
      description: descriptor.description,
      deployClass: false,
      // The envelope a client approves is the one the descriptor declared, so
      // approving a plan cannot widen it past what the flow asked for.
      envelope: { capabilities: descriptor.capabilities, flows: descriptor.flows, budget: {} }
    }))

    /**
     * The plane's own database: plans, approvals, and its projection of each
     * run. It is not the engine's, because a plane run row and an engine run
     * row are different documents that would otherwise collide on one key.
     */
    const planeStores = RunStore.layer.pipe(
      Layer.provideMerge(RunStoreMigrations.layer),
      Layer.provideMerge(DurableWriter.layer()),
      Layer.provideMerge(NodeDatabase.layer({ filename: join(root, "control.sqlite") }))
    )

    // `Layer.provide`, not `provideMerge`: the plane's run store stays inside
    // the plane. Exporting it would put two different `RunStore`s in one
    // context, and everything below would read whichever won.
    const controlRuntime = SqlControlRuntime.layer({
      flows,
      owner: { hostId: "examples-gateway", pid: 1, nonce: "gateway" }
    }).pipe(Layer.provide(planeStores), Layer.orDie)

    /**
     * The acceptance port, wired to the durable engine.
     *
     * It is handed the stored plan and the run row the plane just minted, and
     * it starts THAT run: `run.runId` is the execution id, so the events the
     * engine journals and the row the plane projects name one run.
     *
     * Then it mirrors. The plane cannot see into the engine's database, so an
     * executor that walked away after starting the run would leave every run
     * reading `running` forever. Reading the engine's own row back and writing
     * the plane's vocabulary onto the plane's row is the whole of that duty:
     * the engine calls a parked run `suspended`, an operator calls it `parked`.
     */
    const executorLayer = Layer.effect(ControlExecutor.ControlExecutor)(
      Effect.gen(function*() {
        const plane = yield* ControlRuntime.ControlRuntime
        const services = yield* Effect.context<
          | FlowRuntime.FlowRuntime
          | Crypto.Crypto
          | Registry.Registry
          | RunStore.RunStore
          | FileSystem.FileSystem
          | Path.Path
        >()

        const mirror = (runId: string) =>
          Effect.gen(function*() {
            const runs = yield* RunStore.RunStore
            const row = yield* runs.get(runId)
            const fence = yield* plane.claimFence(runId as ControlSchema.RunId)
            yield* plane.writeStatus(runId as ControlSchema.RunId, fence, planeStatus(row.status))
          }).pipe(Effect.orDie)

        return ControlExecutor.makeNoop({
          launch: ({ plan, run }) =>
            Effect.gen(function*() {
              launched.push(plan.card.flowId)
              const executable = yield* Executable.fromRegistry(plan.card.flowId, bridge)
              const start = yield* Deferred.make<void>()
              const fiber = Effect.runForkWith(services)(
                Deferred.await(start).pipe(
                  Effect.andThen(launch(executable, plan.decodedInput as Schema.Json, run.runId)),
                  Effect.andThen(mirror(run.runId))
                )
              )
              driving.push({ runId: run.runId, start, fiber })
              return "accepted" as const
            }).pipe(Effect.provide(services), Effect.orDie)
        })
      })
    ).pipe(Layer.provideMerge(controlRuntime))

    const controlPlane = ControlLive.layer.pipe(
      Layer.provideMerge(Layer.merge(executorLayer, NotificationQueue.layer))
    )

    // Every discovered descriptor is registered as a durable flow, beside the
    // delegate they name. `Executable.layer` reads the catalog off the same
    // registry the plane lists from, so nothing here names a flow twice.
    const registrations = Layer.mergeAll(
      WaitFor.layer,
      Interpreter.layer(Ship),
      Executable.layer(bridge).pipe(Layer.orDie)
    ).pipe(Layer.provideMerge(Action.layerImplementations))

    // The engine, and the journal both halves write to. The plane keeps its own
    // run table above; it shares this journal, so one `watch` stream carries the
    // plane's decisions and the engine's execution in the order they happened.
    const stack = Layer.merge(controlPlane, registrations).pipe(
      Layer.provideMerge(durableEngine(join(root, "engine.sqlite"), "examples-gateway")),
      Layer.provideMerge(Layer.merge(registry, platform))
    )

    return yield* Effect.scoped(
      Effect.gen(function*() {
        // The server: the same `Control` service, mounted as RPC.
        const served = HttpRouter.serve(
          ControlServer.layerHttp.pipe(
            // Trusted loopback callers act as the local operator. Authentication
            // alone does not delegate approval authority to a test identity.
            Layer.provide(ControlRpcs.layerNoopAuth({ id: "local", kind: "operator", stampedAt: 0 })),
            Layer.provide(RpcSerialization.layerNdjson)
          ),
          { disableListenLog: true, disableLogger: true }
        ).pipe(
          Layer.provideMerge(
            NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 0 })
          )
        )

        return yield* Effect.gen(function*() {
          const server = yield* HttpServer.HttpServer
          const url = addressOf(server)

          // The client: the same `Control` interface, over two transports.
          const client = ControlClient.layer({ url: `${url}/rpc` }).pipe(
            Layer.provide([
              NodeHttpClient.layerUndici,
              NodeSocket.layerWebSocket(`ws://127.0.0.1:${new URL(url).port}/rpc/ws`),
              RpcSerialization.layerNdjson
            ])
          )

          return yield* Effect.gen(function*() {
            const control = yield* Control.Control

            // What this host can run, asked for from the other end of the wire.
            const catalogued = yield* control.list({ _tag: "flows" })
            const catalog = catalogued._tag === "flows" ? catalogued.items.map((flow) => flow.flowId) : []

            const card = yield* control.plan({
              flowId: discoveredFlow as ControlSchema.FlowId,
              input: { build: "v2.0.0" }
            })
            const launch = {
              _tag: "Plan" as const,
              planId: card.planId,
              digest: card.digest,
              envelope: card.envelope
            }
            const beforeApproval = yield* control.run({
              ...launch,
              idempotencyKey: "remote:before" as ControlSchema.IdempotencyKey
            })
            yield* control.approve({
              target: { _tag: "Plan", planId: card.planId, digest: card.digest, envelope: card.envelope },
              scope: card.approval.scope,
              idempotencyKey: "remote:approve" as ControlSchema.IdempotencyKey
            })
            const afterApproval = yield* control.run({
              ...launch,
              idempotencyKey: "remote:after" as ControlSchema.IdempotencyKey
            })
            const watchedRunId = afterApproval._tag === "Accepted" ? afterApproval.runId : undefined

            // The receipt is in hand, so the plane has finished writing
            // `running`. Releasing the latch now lets the run reach its durable
            // wait, and joining it makes the park observable rather than racy.
            yield* Effect.forEach(driving, (started) =>
              Deferred.succeed(started.start, void 0).pipe(
                Effect.andThen(Fiber.join(started.fiber))
              ))

            const listed = yield* control.list({ _tag: "runs", filters: {} })
            const runs = listed._tag === "runs" ? listed.items : []

            // `follow: false` asks for a finite snapshot of what is already
            // durable, which is what makes this assertable. Omitting it opens
            // the live stream a UI subscribes to and never ends.
            const watched = watchedRunId === undefined ? [] : yield* control.watch({
              runId: watchedRunId,
              follow: false
            }).pipe(
              Stream.map((event) => event.kind),
              Stream.runCollect
            )

            return {
              url,
              catalog,
              plannedFlow: card.flowId,
              plannedEnvelope: card.envelope.flows,
              beforeApproval: beforeApproval._tag,
              afterApproval: afterApproval._tag,
              watchedRunId,
              launched: [...launched],
              listed: runs.map((run) => run.runId),
              parked: runs.find((run) => run.runId === watchedRunId)?.status,
              watched: [...watched]
            } satisfies Summary
          }).pipe(Effect.provide(client))
        }).pipe(Effect.provide(served))
      }).pipe(Effect.provide(Layer.merge(stack, NodeCrypto.layer)))
    )
  }).pipe(Effect.orDie)
```

---

# Agent tools in sandbox
https://smithers.sh/docs/examples/25-agent-tools-in-sandbox/

Run a scripted agent with filesystem tools inside a bounded QuickJS cell loop.

The cell reaches tools through registered flows. The host supplies a capability envelope, the sandbox limits cell evaluation, and durable call boundaries record tool outcomes for replay.

The standard filesystem flows operate on a scratch directory through the supplied host service. The scripted seat makes the test deterministic and requires no API key.

## Run the test

The complete program is [`examples/src/25-agent-tools-in-sandbox.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/25-agent-tools-in-sandbox.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/25-agent-tools-in-sandbox.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/capability`](/docs/reference/api/capability/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/25-agent-tools-in-sandbox.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import * as StandardFlows from "@smthrs/agent/StandardFlows"
import * as Capability from "@smthrs/capability/Capability"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import * as Effect from "effect/Effect"
import type * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Path from "effect/Path"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { durableEngine } from "./durable-layer.ts"

/** What the step must answer with. Nothing downstream parses model text. */
export const Tally = Schema.Struct({
  totalLines: Schema.Number,
  wrotePath: Schema.String,
  bytesWritten: Schema.Number
})

/**
 * The step. Its prompt names the two paths, which is how a scripted model, and
 * a real one, learns which files this task is about.
 */
export const Summarize = AgentAction.make("examples/SandboxSummarize", {
  payload: { source: Schema.String, target: Schema.String },
  output: Tally,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You maintain a repository. Use the file tools rather than guessing at contents."],
  prompt: ({ source, target }) =>
    `Count the lines in the source file and write the count to the target file.\nSOURCE: ${source}\nTARGET: ${target}`
})

/** The flow that runs the one step whose cell reaches the file tools. */
export const Audit = Flow.make("examples/Audit", {
  payload: { source: Schema.String, target: Schema.String },
  success: Tally,
  error: AgentAction.AgentFailure,
  body: (payload: { readonly source: string; readonly target: string }) => Summarize.call(payload)
})

const prepared: Route.PreparedRequest = {
  routeId: "examples",
  protocolId: "examples",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

/**
 * A scripted model that reads the two paths out of the request and answers with
 * one cell that uses the tools.
 *
 * The paths are not baked in: they arrive in the prompt at run time, exactly as
 * they would for a provider, and the cell the model writes closes over what it
 * read. That is what makes this a scripted MODEL rather than a scripted answer.
 */
export const scripted = (calls: Array<string>): Model.Model =>
  Model.make({
    stream: (request) =>
      Stream.suspend(() => {
        const asked = [
          ...request.system.map((part) => part.text),
          ...request.messages.flatMap((message) =>
            message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
          )
        ].join("\n")
        const source = /SOURCE: (.+)/.exec(asked)?.[1]?.trim() ?? ""
        const target = /TARGET: (.+)/.exec(asked)?.[1]?.trim() ?? ""
        calls.push(`${source} -> ${target}`)
        const cell = [
          `const page = await ctx.call("read", { path: ${JSON.stringify(source)} })`,
          `const written = await ctx.call("write", {`,
          `  path: ${JSON.stringify(target)},`,
          "  content: String(page.totalLines) + \"\\n\"",
          "})",
          "ctx.done({",
          "  totalLines: page.totalLines,",
          "  wrotePath: written.path,",
          "  bytesWritten: written.bytesWritten",
          "})"
        ].join("\n")
        return Stream.fromIterable([
          ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
          ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
          ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
          ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
        ])
      })
  })

/** What one run of the step observed. */
export interface Summary {
  /** The schema-typed answer the step settled with. */
  readonly tally: typeof Tally.Type
  /** The file the cell wrote, read back off the real disk. */
  readonly written: string
  /** The prompts the model saw, as `source -> target`. */
  readonly asked: ReadonlyArray<string>
  /** The distinct events the run journalled. */
  readonly eventTypes: ReadonlyArray<string>
}

/**
 * Runs the step against a real directory.
 *
 * @param filename the SQLite file the durable engine runs on
 * @param root a directory the run may read and write
 */
export const main = (filename: string, root: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const source = join(root, "notes.md")
    const target = join(root, "line-count.txt")
    yield* Effect.sync(() => {
      mkdirSync(root, { recursive: true })
      writeFileSync(source, "alpha\nbeta\ngamma")
    })

    // The host's filesystem, handed to the bindings as a context. A browser host
    // supplies a different one and the cell sees no difference.
    const services = yield* Effect.context<FileSystem.FileSystem | Path.Path>()
    const asked: Array<string> = []

    const host = AgentAction.layerHost({
      // The catalog the model is shown, and the registry its calls resolve
      // against. It holds nothing of its own here: every capability comes from
      // the bound sources below.
      registry: Registry.makeNoop({
        list: () => Effect.succeed([]),
        visible: () => Effect.succeed([]),
        getOption: () => Effect.succeed(Option.none())
      }),
      flows: [StandardFlows.filesystem(services)],
      // The explicit sandbox budget every cell in this composition runs under.
      limits: { calls: 8 },
      // The standard flows declare real capabilities, so the run needs a real
      // envelope. An empty one refuses every declared capability by contract,
      // which is the safe default rather than an oversight.
      capabilityEnvelope: [new Capability.CapabilityPattern({ action: "*", resource: "*" })],
      maxFrames: 3
    })

    const seats = SeatResolver.layer({
      resolve: (id) =>
        Effect.succeed(
          Seat.make({
            id,
            model: scripted(asked),
            route: { prepare: () => Effect.succeed(prepared) },
            contextWindowTokens: 200_000
          })
        )
    })

    const stack = Layer.mergeAll(Summarize.layer, Interpreter.layer(Audit)).pipe(
      Layer.provideMerge(Layer.mergeAll(host, seats, Agent.layer)),
      // The scripted seat cannot refuse for quota, and this offline example
      // has no approved plan envelope from which to derive a ceiling.
      // eslint-disable-next-line no-restricted-syntax -- this offline example has no approved envelope
      Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerUnclassified(), Budget.layerUnbounded())),
      // The QuickJS sandbox the cell's code runs in, and the steering source it
      // drains. Both are browser-safe defaults.
      Layer.provideMerge(Agent.layerDefaults),
      Layer.provideMerge(Action.layerImplementations),
      Layer.provideMerge(durableEngine(filename, "examples-sandbox"))
    )

    const observed = yield* Effect.scoped(
      Effect.gen(function*() {
        const tally = yield* Audit.execute({ source, target }, { executionId: "audit-1" })
        const journal = yield* Journal.Journal
        yield* journal.flush
        const page = yield* journal.entries({ runId: "audit-1" as JournalEvent.RunId, limit: 500 })
        return { tally, eventTypes: [...new Set(page.entries.map((entry) => entry.eventType))] }
      }).pipe(Effect.provide(stack))
    )

    return {
      tally: observed.tally,
      written: readFileSync(target, "utf8"),
      asked,
      eventTypes: observed.eventTypes
    } satisfies Summary
  }).pipe(
    Effect.provide(Layer.mergeAll(NodeFileSystem.layer, Path.layer, NodeCrypto.layer)),
    Effect.orDie
  )
```

---

# Memory recall
https://smithers.sh/docs/examples/26-memory-recall/

Store facts in one run and recall them in a later execution.

Separate durable runs share the SQL memory store. `WithMemory.withMemory` applies namespace, recall budget, and retention policy to a flow tree. The handler source must use the annotated flow so those policies reach the store.

The scenarios also exercise `recall: "none"` and `retain: "never"`. Recall uses SQLite FTS5, enabled for the relevant namespace kind. A scripted model keeps the example independent of provider access.

## Run the test

The complete program is [`examples/src/26-memory-recall.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/26-memory-recall.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/26-memory-recall.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/harness`](/docs/reference/api/harness/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/memory`](/docs/reference/api/memory/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/26-memory-recall.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as FlowBinding from "@smthrs/harness/FlowBinding"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as MemoryFlows from "@smthrs/memory/Flows"
import * as MemoryStore from "@smthrs/memory/MemoryStore"
import * as Recall from "@smthrs/memory/Recall"
import * as RecallFts from "@smthrs/memory/RecallFts"
import * as WithMemory from "@smthrs/memory/WithMemory"
import * as Model from "@smthrs/model/Model"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import type * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { durableEngine } from "./durable-layer.ts"

/** The bank this example's flow tree reads and writes. */
export const namespace = { kind: "flow", id: "release-notes" } as const

/** The bank name the namespace is spelled as at the recall boundary. */
export const bank: string = Recall.bankForNamespace(namespace)

/** The policy every run below carries, before a refusal overrides a field. */
export const basePolicy: WithMemory.Policy = {
  namespace,
  recall: "auto",
  maxTokens: 2048,
  retain: "on-complete"
}

/**
 * The two memory flows, bound under one policy, as a source a cell can call.
 *
 * This is the shape `Flows.handlersFor` documents and the reason it exists: the
 * declaration a host binds is the policy-carrying COPY, and the handler is
 * built from that same copy. `StandardFlows.memory` is the same pairing over
 * the bare declarations, which is right for a host that has no policy to apply
 * and wrong for one that does.
 */
export const memorySource = (
  services: Context.Context<MemoryStore.MemoryStore | Recall.Recall>,
  policy: WithMemory.Policy
): FlowBinding.Source => {
  const remember = WithMemory.withMemory(MemoryFlows.remember, policy)
  const recall = WithMemory.withMemory(MemoryFlows.recall, policy)
  return FlowBinding.source("memory", [
    FlowBinding.provide(
      FlowBinding.make({ flow: remember, handler: MemoryFlows.handlersFor(remember).remember }),
      services
    ),
    FlowBinding.provide(
      FlowBinding.make({ flow: recall, handler: MemoryFlows.handlersFor(recall).recall }),
      services
    )
  ])
}

/** What one notebook step must answer with. Nothing downstream parses prose. */
export const Reading = Schema.Struct({ keys: Schema.Array(Schema.String) })

/**
 * The step. Its prompt names the task, which is how a scripted model, and a
 * real one, learns whether this run is writing or reading.
 */
export const Notebook = AgentAction.make("examples/Notebook", {
  payload: { task: Schema.String, note: Schema.String },
  output: Reading,
  seat: "anthropic:claude-sonnet-4-5",
  system: [
    "You keep release notes. Write down what you learn, and look it up later rather than guessing."
  ],
  prompt: ({ note, task }) => `TASK: ${task}\nNOTE: ${note}`
})

/** The flow one run of the notebook step is. */
export const Keep = Flow.make("examples/Keep", {
  payload: { task: Schema.String, note: Schema.String },
  success: Reading,
  error: AgentAction.AgentFailure,
  body: (payload: { readonly task: string; readonly note: string }) => Notebook.call(payload)
})

/** The facts run 1 records. */
export const facts: ReadonlyArray<{ readonly key: string; readonly text: string }> = [
  { key: "changelog-format", text: "Release notes group entries by package, newest first." },
  { key: "release-cadence", text: "Release notes ship with every tagged release, never between." },
  { key: "review-owner", text: "The docs owner reviews the notes before the tag is pushed." }
]

const prepared: Route.PreparedRequest = {
  routeId: "examples",
  protocolId: "examples",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

/** The cell body the model writes for one task. */
const cellFor = (task: string, note: string): string => {
  if (task === "record") {
    return [
      ...facts.map((fact) =>
        // The bank is empty on purpose: the policy fills it in. A caller that
        // names its own keeps it.
        `await ctx.call("remember", { bank: "", key: ${JSON.stringify(fact.key)}, text: ${JSON.stringify(fact.text)} })`
      ),
      `ctx.done({ keys: ${JSON.stringify(facts.map((fact) => fact.key))} })`
    ].join("\n")
  }
  if (task === "forget") {
    return [
      `const dropped = await ctx.call("remember", { bank: "", key: "never-stored", text: ${JSON.stringify(note)} })`,
      "ctx.done({ keys: [dropped.key] })"
    ].join("\n")
  }
  const banks = task === "foreign" ? ["flow-other-project"] : []
  return [
    // No banks and no budget for the scoped read: both come from the policy.
    `const rows = await ctx.call("recall", { banks: ${JSON.stringify(banks)}, query: ${JSON.stringify(note)} })`,
    "ctx.done({ keys: rows.map((row) => row.key) })"
  ].join("\n")
}

/**
 * A scripted model that reads the task out of the request and answers with one
 * cell that uses the memory flows.
 *
 * The task is not baked in: it arrives in the prompt at run time, exactly as it
 * would for a provider, and the cell the model writes closes over what it read.
 */
export const scripted = (asked: Array<string>): Model.Model =>
  Model.make({
    stream: (request) =>
      Stream.suspend(() => {
        const text = [
          ...request.system.map((part) => part.text),
          ...request.messages.flatMap((message) =>
            message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
          )
        ].join("\n")
        const task = /TASK: (.+)/.exec(text)?.[1]?.trim() ?? ""
        const note = /NOTE: (.+)/.exec(text)?.[1]?.trim() ?? ""
        asked.push(task)
        const cell = cellFor(task, note)
        return Stream.fromIterable([
          ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
          ModelEvent.ModelEvent.TextDelta({ type: "text-delta", id: "cell", text: "```cell\n" + cell + "\n```" }),
          ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
          ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
        ])
      })
  })

/** What six durable runs over one memory bank observed. */
export interface Summary {
  /** The keys the writing run recorded, in order. */
  readonly written: ReadonlyArray<string>
  /** The keys a LATER run recalled, having been handed nothing but the file. */
  readonly recalled: ReadonlyArray<string>
  /** What recall answered for a bank nothing was written to. */
  readonly foreign: ReadonlyArray<string>
  /** What a policy of `recall: "none"` answered. */
  readonly refusedRecall: ReadonlyArray<string>
  /** The key a policy of `retain: "never"` answered with, having stored nothing. */
  readonly droppedWriteKey: string | undefined
  /** Whether the dropped write left a row a later run can find. */
  readonly droppedWriteStored: boolean
  /** The tasks the model was asked to do, in run order. */
  readonly asked: ReadonlyArray<string>
  /** The distinct events the recalling run journalled. */
  readonly eventTypes: ReadonlyArray<string>
}

/**
 * Runs the notebook six times over one SQLite file.
 *
 * Each call is a separate durable execution: its own run row, its own journal,
 * its own frames. The only thing they share is the memory the store holds.
 */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const asked: Array<string> = []

    // The store and the FTS recall service, over the engine's own database.
    const stores = Layer.provideMerge(RecallFts.layer, Layer.orDie(MemoryStore.layer))
    const base = stores.pipe(Layer.provideMerge(durableEngine(filename, "examples-memory")))

    const seats = SeatResolver.layer({
      resolve: (id) =>
        Effect.succeed(
          Seat.make({
            id,
            model: scripted(asked),
            route: { prepare: () => Effect.succeed(prepared) },
            contextWindowTokens: 200_000
          })
        )
    })

    const registry = Registry.makeNoop({
      list: () => Effect.succeed([]),
      visible: () => Effect.succeed([]),
      getOption: () => Effect.succeed(Option.none())
    })

    return yield* Effect.scoped(
      Effect.gen(function*() {
        const store = yield* MemoryStore.MemoryStore
        // FTS is enabled per namespace kind, by the store, once.
        yield* store.enableFts("flow")

        // The services the bindings run their handlers in. A host with a
        // different store composes a different context and the cell sees no
        // difference.
        const services = yield* Effect.context<MemoryStore.MemoryStore | Recall.Recall>()

        /** One durable run of the notebook, under one memory policy. */
        const run = (
          executionId: string,
          task: string,
          note: string,
          policy: WithMemory.Policy = basePolicy
        ) =>
          Keep.execute({ task, note }, { executionId }).pipe(
            Effect.provide(
              Layer.mergeAll(Notebook.layer, Interpreter.layer(Keep)).pipe(
                Layer.provideMerge(
                  Layer.mergeAll(
                    AgentAction.layerHost({
                      registry,
                      flows: [memorySource(services, policy)],
                      limits: { calls: 8 },
                      capabilityEnvelope: [],
                      maxFrames: 2
                    }),
                    seats,
                    Agent.layer
                  )
                ),
                // The scripted seat has no provider quota, and these offline
                // runs have no approved plan envelope to derive a ceiling.
                // eslint-disable-next-line no-restricted-syntax -- these offline runs have no approved envelope
                Layer.provideMerge(Layer.mergeAll(QuotaPolicy.layerUnclassified(), Budget.layerUnbounded())),
                Layer.provideMerge(Agent.layerDefaults),
                Layer.provideMerge(Action.layerImplementations)
              )
            )
          )

        const written = yield* run("notes-1", "record", "the three release-note rules")
        const recalled = yield* run("notes-2", "recall", "release notes")
        const foreign = yield* run("notes-3", "foreign", "release notes")
        const refused = yield* run("notes-4", "recall", "release notes", {
          ...basePolicy,
          recall: "none"
        })
        const dropped = yield* run("notes-5", "forget", "Release notes are written by hand.", {
          ...basePolicy,
          retain: "never"
        })
        const after = yield* run("notes-6", "recall", "hand")

        const journal = yield* Journal.Journal
        yield* journal.flush
        const page = yield* journal.entries({ runId: "notes-2" as JournalEvent.RunId, limit: 500 })

        return {
          written: written.keys,
          recalled: recalled.keys,
          foreign: foreign.keys,
          refusedRecall: refused.keys,
          droppedWriteKey: dropped.keys[0],
          droppedWriteStored: after.keys.includes("never-stored"),
          asked: [...asked],
          eventTypes: [...new Set(page.entries.map((entry) => entry.eventType))]
        } satisfies Summary
      }).pipe(Effect.provide(base))
    )
  }).pipe(
    Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodeCrypto.layer)),
    Effect.orDie
  )
```

---

# Failure control
https://smithers.sh/docs/examples/30-failure-control/

Failure control: what runs, what unwinds, and what the plan shows first.

`@smthrs/patterns` gives every container two halves. `make` builds the conservative topology (every rung, member, and compensation that could be reached) so a reader sees the failure paths before anything runs. `run` is the Effect that performs the value-dependent branch.

This example releases a build: bounded checks, a quarantined flake, an escalating fixer, a saga that unwinds a half-finished deploy, and a lock the finalizer always releases.

## Run the test

The complete program is [`examples/src/30-failure-control.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/30-failure-control.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/30-failure-control.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/patterns`](/docs/reference/api/patterns/)

## Source

```ts title="examples/src/30-failure-control.ts"
import { Flow, Graph, Node } from "@smthrs/core"
import { Bounded, Escalation, PatternError, Quarantine, Saga, TryCatchFinally } from "@smthrs/patterns"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"

/** The failure the deploy's third step raises. */
export class Rejected extends Schema.TaggedError<Rejected>()("examples/Rejected", {
  step: Schema.String
}) {}

/** A declared call that echoes its own name, so a built graph can name it. */
const call = (name: string) =>
  Flow.make({
    input: Schema.Unknown,
    output: Schema.Unknown,
    body: Node.capture({ name }, () => Node.succeed({ from: name }))
  })

/** The deploy, declared. Nothing runs; this is the plan the operator reads. */
export const deployment = Saga.make({
  steps: [
    { id: "reserve", action: call("reserve"), compensation: call("release") },
    { id: "upload", action: call("upload"), compensation: call("delete") },
    { id: "activate", action: call("activate"), compensation: call("roll-back") }
  ],
  onFailure: "compensate"
})

/** Every call the declaration reaches, forward steps then compensations. */
export const declaredCalls: ReadonlyArray<string> = Graph.nodes(Graph.build(deployment, { release: "v1" }))
  .filter((node) => node.kind === "Succeed" && node.id.endsWith(".flow"))
  .map((node) => (node.keyMaterial.body as { readonly value: { readonly from: string } }).value.from)

/** What the release did, in the order it did it. */
export interface Summary {
  readonly checks: Readonly<Record<string, string>>
  readonly quarantined: ReadonlyArray<string>
  readonly fixedAt: number
  readonly deploy: ReadonlyArray<string>
  readonly outcome: string
  readonly lockHeld: boolean
}

/**
 * The failures this release can end in: a pattern's own declaration or runtime
 * failure, and the deploy's typed rejection.
 */
export type ReleaseError = PatternError.PatternError | Rejected

export const main = (): Effect.Effect<Summary, ReleaseError> =>
  Effect.gen(function*() {
    const trace: Array<string> = []
    let lockHeld = true
    const record = <A>(name: string, value: A): Effect.Effect<A> =>
      Effect.sync(() => {
        trace.push(name)
        return value
      })

    // Two checks at a time, the release blocker first.
    const check = (name: string) => Effect.sync(() => `${name}-clean`)
    const checks = yield* Bounded.run(
      { lint: check("lint"), types: check("types"), audit: check("audit") },
      { concurrency: 2, priorities: { audit: 9 } }
    )

    // A flaky check must not interrupt the two beside it.
    const settled = yield* Quarantine.run({
      unit: check("unit"),
      flake: Effect.fail(new Rejected({ step: "flake" })),
      e2e: check("e2e")
    }, { policy: "quarantine" })
    const quarantined = Object.keys(settled).filter((member) => Quarantine.isQuarantined(settled[member]))

    // Try the cheap fixer, then the thorough one. The ladder stops at the
    // first rung whose result does not report a failure.
    const fixed = yield* Escalation.run({ report: quarantined }, {
      rungs: [
        () => Effect.succeed({ ok: false, by: "formatter" }),
        () => Effect.succeed({ ok: true, by: "rewriter" })
      ]
    })

    // The deploy holds a lock, and the finalizer releases it on every path.
    const outcome = yield* TryCatchFinally.run({ release: "v1" }, {
      try: (input: { readonly release: string }) =>
        Saga.run(input, {
          steps: [
            {
              id: "reserve",
              action: () => record("reserve", "slot-7"),
              compensation: () => record("release", undefined)
            },
            {
              id: "upload",
              action: () => record("upload", "bundle-9"),
              compensation: () => record("delete", undefined)
            },
            {
              id: "activate",
              action: () =>
                Effect.flatMap(
                  record("activate", undefined),
                  () => Effect.fail(new Rejected({ step: "activate" }))
                ),
              compensation: () => record("roll-back", undefined)
            }
          ],
          onFailure: "compensate"
        }),
      finally: () =>
        Effect.sync(() => {
          lockHeld = false
        })
    })

    return {
      checks,
      quarantined,
      fixedAt: fixed.level,
      deploy: trace,
      outcome: "compensated" in outcome ? "compensated" : "released",
      lockHeld
    }
  })
```

---

# Bounded loops
https://smithers.sh/docs/examples/31-bounded-loops/

Repair a configuration and refine a summary with bounded loop patterns.

The example calls each pattern's runtime `run` operation, which stops when its value-dependent condition is met. The declaration-side `make` operation describes the conservative graph with its bound.

The fixtures are deterministic and need no model, durable engine, or network. See the patterns API reference for the declaration forms.

## Run the test

The complete program is [`examples/src/31-bounded-loops.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/31-bounded-loops.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/31-bounded-loops.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/patterns`](/docs/reference/api/patterns/)

## Source

```ts title="examples/src/31-bounded-loops.ts"
import * as DriftDetector from "@smthrs/patterns/DriftDetector"
import * as Loop from "@smthrs/patterns/Loop"
import * as Optimizer from "@smthrs/patterns/Optimizer"
import * as ScanFixVerify from "@smthrs/patterns/ScanFixVerify"
import * as Sidecar from "@smthrs/patterns/Sidecar"
import * as Effect from "effect/Effect"

interface Setting {
  readonly key: string
  readonly value: number
}

interface Violation {
  readonly key: string
  readonly value: number
  readonly limit: number
}

const limits: Readonly<Record<string, number>> = { retries: 3, timeoutMs: 30_000, concurrency: 8 }

/**
 * A config the repair loop edits in place, the way a real fixer edits files.
 */
const config = (): Map<string, number> =>
  new Map([["retries", 9], ["timeoutMs", 90_000], ["concurrency", 4]])

const violations = (current: ReadonlyMap<string, number>): ReadonlyArray<Violation> =>
  [...current].flatMap(([key, value]) => {
    const limit = limits[key]
    return limit !== undefined && value > limit ? [{ key, value, limit }] : []
  })

/**
 * Scans a config for settings above policy, repairs each one on its own, and
 * verifies the result, until a scan comes back clean.
 *
 * `retries` and `timeoutMs` both start above their limits, so the first round
 * fixes two settings concurrently and the verifier reports them resolved. That
 * verdict does not end the loop: the second round rescans, finds nothing, and
 * that empty scan is the terminal. Two rounds, one of them a confirmation.
 * `remaining` is empty on a clean exit and lists the last scan's issues when
 * the retry bound stopped the loop, which is the signal an operator acts on.
 */
export const repair: Effect.Effect<{
  readonly iterations: number
  readonly remaining: ReadonlyArray<Violation>
  readonly settled: ReadonlyArray<Setting>
}> = Effect.gen(function*() {
  const current = config()
  const report = yield* ScanFixVerify.run(current, {
    maxRetries: 4,
    concurrency: 4,
    scan: ({ input }) => Effect.succeed(violations(input)),
    fix: ({ issue }) => Effect.sync(() => (current.set(issue.key, issue.limit), issue.key)),
    verify: ({ input }) => Effect.succeed({ resolved: violations(input).length === 0 })
  }).pipe(
    // `run` validates its bounds before scanning and fails `PatternError` when
    // they are not positive integers. They are literals here, so a refusal
    // would be a defect in this example rather than an error a caller handles.
    Effect.orDie
  )
  return {
    iterations: report.iterations,
    remaining: report.remaining,
    settled: [...current].map(([key, value]) => ({ key, value }))
  }
})

/**
 * Compares the repaired config to the baseline an operator recorded, and pages
 * only when it moved.
 * The baseline is the config as last recorded, so the repair is exactly what
 * the detector reports: two settings moved away from what the operator
 * approved, and the alert names them.
 *
 * The detector runs once. Polling is the caller's decision: wrap this in
 * `Loop.run` for rounds inside one execution, or register the flow on a
 * schedule for rounds the control plane owns.
 */
export const audit = (
  settled: ReadonlyArray<Setting>
): Effect.Effect<{ readonly drifted: boolean; readonly paged: ReadonlyArray<string> }> =>
  Effect.gen(function*() {
    const paged: Array<string> = []
    const result = yield* DriftDetector.run(settled, {
      baseline: { retries: 9, timeoutMs: 90_000, concurrency: 4 },
      capture: ({ input }) => Effect.succeed(Object.fromEntries(input.map((s) => [s.key, s.value]))),
      compare: ({ baseline, snapshot }) =>
        Effect.succeed({
          drifted: Object.entries(baseline).some(([key, value]) => snapshot[key] !== value),
          changed: Object.entries(baseline).filter(([key, value]) => snapshot[key] !== value).map(([key]) => key)
        }),
      alert: ({ comparison }) => Effect.sync(() => (paged.push(...comparison.changed), comparison.changed.length))
    })
    return { drifted: result.drifted, paged }
  })

/**
 * The candidates the search produces, in order. The second is the shortest, so
 * the last attempt is not the best one.
 */
export const wordy: ReadonlyArray<string> = [
  "the config was repaired successfully by the loop",
  "repaired",
  "config repaired"
]

/**
 * Searches for a summary short enough to fit an alert line.
 *
 * The scorer rewards brevity and no candidate reaches the target, so the search
 * spends its whole budget. It still answers with the shortest summary it saw
 * rather than the one it happened to end on. Keeping the best attempt is the
 * difference between an optimizer and a retry.
 */
export const tune: Effect.Effect<{
  readonly best: string
  readonly score: number
  readonly iterations: number
  readonly converged: boolean
}> = Effect.gen(function*() {
  const result = yield* Optimizer.run("summary", {
    maxIterations: 3,
    targetScore: 0.9,
    onMaxReached: "return-last",
    generate: ({ iteration }) => Effect.succeed(wordy[iteration - 1] ?? ""),
    // The candidate type is annotated because `generate` reads `previous`,
    // which makes its own parameter depend on the candidate type. TypeScript
    // then has nothing to infer that type from when it types `evaluate`. See
    // the reference page, "Inline callbacks and inference".
    evaluate: ({ value }: { readonly value: string }) =>
      Effect.succeed({ score: 1 - value.length / 60, feedback: "shorter" })
  }).pipe(Effect.orDie)
  return {
    best: result.best.candidate,
    score: Number(result.best.score.toFixed(4)),
    iterations: result.iterations,
    converged: result.converged
  }
})

/**
 * Runs a cheap shadow beside the expensive summary to see whether the cheap
 * seat would have been good enough.
 *
 * The shadow here fails. A sidecar quarantines that, because the shadow is an
 * experiment and the primary result is the answer. `delta` stays absent, since
 * there is nothing to compare against.
 */
export const shadow: Effect.Effect<{
  readonly primary: string
  readonly quarantined: boolean
  readonly delta: number | undefined
}> = Effect.gen(function*() {
  const result = yield* Sidecar.run("summary", {
    primary: () => Effect.succeed("config repaired"),
    // Annotated because this shadow only fails: without it the shadow output
    // type infers as `never` and the scorer cannot read it.
    shadow: (): Effect.Effect<string, string> => Effect.fail("the cheap seat is out of quota"),
    score: ({ primary, shadow }) => Effect.succeed({ primary: primary.length, shadow: shadow.length })
    // `Sidecar.run` refuses an unsafe configuration with a `PatternError`, the
    // same way `Optimizer.run` above does. Every input here is a literal, so a
    // refusal would be a defect in this file rather than a run-time outcome.
  }).pipe(Effect.orDie)
  return {
    primary: result.primary,
    quarantined: result.shadow.quarantined,
    delta: result.delta?.difference
  }
})

/**
 * Keeps handing the same goal back until the body reports itself finished.
 *
 * This is the Ralph loop: no separate predicate, `maxIterations` as the budget,
 * and `exhausted` telling the caller which of the two ended the loop. It omits
 * `onMaxReached`, which defaults to `"return-last"`.
 */
export const settle: Effect.Effect<{ readonly turns: number; readonly exhausted: boolean }> = Effect.gen(function*() {
  let pending = 3
  const result = yield* Loop.runRalph("drain the queue", {
    maxIterations: 10,
    body: () => Effect.sync(() => ({ done: --pending <= 0, pending }))
  }).pipe(Effect.orDie)
  return { turns: result.iterations, exhausted: result.exhausted }
})

/**
 * Runs the whole story: repair, audit, tune, shadow, settle.
 */
export const main: Effect.Effect<{
  readonly repair: { readonly iterations: number; readonly remaining: ReadonlyArray<Violation> }
  readonly audit: { readonly drifted: boolean; readonly paged: ReadonlyArray<string> }
  readonly tune: { readonly best: string; readonly converged: boolean }
  readonly shadow: { readonly quarantined: boolean }
  readonly settle: { readonly turns: number }
}> = Effect.gen(function*() {
  const repaired = yield* repair
  const audited = yield* audit(repaired.settled)
  const tuned = yield* tune
  const shadowed = yield* shadow
  const settled = yield* settle
  return {
    repair: { iterations: repaired.iterations, remaining: repaired.remaining },
    audit: audited,
    tune: { best: tuned.best, converged: tuned.converged },
    shadow: { quarantined: shadowed.quarantined },
    settle: { turns: settled.turns }
  }
})
```

---

# Intervene
https://smithers.sh/docs/examples/32-intervene/

Intervene on a real workspace: read a file, propose an edit, gate the write behind an approval, and report what happened.

The pattern has the two surfaces every `@smthrs/patterns` module has, and this example uses both over the same std flows. `Intervene.make` builds the declaration: the read and apply steps name `Read.flow` and `Edit.flow` from `@smthrs/std` and inherit their capabilities and effect envelopes, and the apply step sits behind `WithApproval`, so a planner can see the gate before anything runs. `Intervene.run` performs the value-dependent part over a real temp directory, through `Read.run` and `Edit.run`, which are the implementations of the very flows the declaration named.

`dryRun` is a declaration-time decision, not a runtime flag the write step is trusted to honor: a dry-run plan has no apply call in it at all, so it cannot reach a writing step even if something later goes wrong.

## Run the test

The complete program is [`examples/src/32-intervene.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/32-intervene.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/32-intervene.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/core`](/docs/reference/api/core/) · [`@smthrs/patterns`](/docs/reference/api/patterns/) · [`@smthrs/std`](/docs/reference/api/std/)

## Source

```ts title="examples/src/32-intervene.ts"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import { Flow, Node } from "@smthrs/core"
import * as Intervene from "@smthrs/patterns/Intervene"
import * as WithApproval from "@smthrs/patterns/WithApproval"
import * as Edit from "@smthrs/std/Edit"
import * as Read from "@smthrs/std/Read"
import * as StdError from "@smthrs/std/StdError"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as PlatformError from "effect/PlatformError"
import * as Schema from "effect/Schema"

/** The rewrite an intervention is asked to perform. */
export const Job = Schema.Struct({
  path: Schema.String,
  from: Schema.String,
  to: Schema.String
})

/** The envelope `Intervene` hands its read flow. */
const ReadCall = Schema.Struct({ phase: Schema.String, input: Job })

/** The envelope `Intervene` hands its propose flow. */
const ProposeCall = Schema.Struct({ phase: Schema.String, input: Job, context: Read.Output })

/** The envelope `Intervene` hands its apply flow, and the approval that gates it. */
const ApplyCall = Schema.Struct({ phase: Schema.String, input: Job, proposal: Edit.Input })

/** The envelope `Intervene` hands its report flow. */
const ReportCall = Schema.Struct({
  phase: Schema.String,
  input: Job,
  proposal: Edit.Input,
  applied: Schema.optional(Edit.Output),
  dryRun: Schema.Boolean
})

/** What the intervention reports when it is done. */
export const Report = Schema.Struct({
  path: Schema.String,
  replacements: Schema.Number,
  dryRun: Schema.Boolean
})

/**
 * Reads the target file. The step names the std read flow and no body, so its
 * body is one dynamic node that has that flow available, and it declares the
 * read flow's capabilities and hermetic read-only envelope as its own.
 */
export const ReadTarget = Flow.make({
  name: "read-target",
  input: ReadCall,
  output: Read.Output,
  capabilities: Read.capabilities,
  effects: Read.effects,
  flows: [Read.flow]
})

/** Turns the job into the edit the apply step performs. This step writes nothing. */
export const ProposeEdit = Flow.make({
  name: "propose-edit",
  input: ProposeCall,
  output: Edit.Input,
  body: ({ input }) => Node.succeed({ path: input.path, oldString: input.from, newString: input.to })
})

/** Writes the proposed edit, under the std edit flow's compensable envelope. */
export const ApplyEdit = Flow.make({
  name: "apply-edit",
  input: ApplyCall,
  output: Edit.Output,
  capabilities: Edit.capabilities,
  effects: Edit.effects,
  flows: [Edit.flow]
})

/** Summarizes the intervention. */
export const ReportEdit = Flow.make({
  name: "report-edit",
  input: ReportCall,
  output: Report,
  body: ({ applied, dryRun, proposal }) =>
    Node.succeed({
      path: proposal.path,
      replacements: applied === undefined ? 0 : applied.replacements,
      dryRun
    })
})

/**
 * The approval that gates the write. The only value it may return is the
 * literal `"approved"`, so a denial fails the typed schema decode instead of
 * reaching the edit.
 */
export const ApproveEdit = Flow.make({
  name: "approve-edit",
  input: Schema.Unknown,
  output: WithApproval.Approved,
  body: () => Node.dynamic({ output: WithApproval.Approved })
})

const declaration = {
  read: ReadTarget,
  propose: ProposeEdit,
  apply: ApplyEdit,
  report: ReportEdit,
  approval: ApproveEdit,
  reason: "rewrite the greeting"
}

/** The gated intervention: read, propose, approve, apply, report. */
export const plan = Intervene.make({ ...declaration, dryRun: false })

/** The same intervention as a dry run: the plan has no apply call in it. */
export const dryPlan = Intervene.make({ ...declaration, dryRun: true })

/**
 * Everything an intervention can fail with. Naming the union keeps the failure
 * channel readable instead of widening it to `unknown`: `PlatformError` comes
 * from the temp directory and the read-back, `StdError` from `Read.run` and
 * `Edit.run`, and `SchemaError` from an approval that does not decode as
 * `"approved"`.
 */
export type Failure = PlatformError.PlatformError | StdError.StdError | Schema.SchemaError

/**
 * Runs an intervention over a fresh temp directory and returns the report next
 * to the file's contents afterwards, so a caller can see whether the write
 * actually happened.
 */
export const intervene = (options: {
  readonly dryRun: boolean
  readonly decision: string
}): Effect.Effect<
  { readonly report: typeof Report.Type; readonly content: string },
  Failure,
  FileSystem.FileSystem
> =>
  Effect.scoped(Effect.gen(function*() {
    const fileSystem = yield* FileSystem.FileSystem
    const directory = yield* fileSystem.makeTempDirectoryScoped({ prefix: "flows-intervene-" })
    const path = `${directory}/greeting.txt`
    yield* fileSystem.writeFileString(path, "Hello, world.\n")

    const report = yield* Intervene.run(
      { path, from: "Hello, world.", to: "Hello, Ada." },
      {
        dryRun: options.dryRun,
        // The declaration named `Read.flow` and `Edit.flow`; execution runs
        // their implementations, `Read.run` and `Edit.run`.
        read: (job) => Read.run({ path: job.path }),
        propose: ({ input }) =>
          Effect.succeed({ path: input.path, oldString: input.from, newString: input.to }),
        apply: ({ proposal }) => Edit.run(proposal),
        report: ({ applied, dryRun, proposal }) =>
          Effect.succeed({
            path: proposal.path,
            replacements: applied === undefined ? 0 : applied.replacements,
            dryRun
          }),
        approval: () => Effect.succeed(options.decision)
      }
    )
    const content = yield* fileSystem.readFileString(path)
    return { report, content }
  }))

/**
 * Applies the rewrite with an approval that answers `"approved"`, on a real
 * Node filesystem.
 */
export const main: Effect.Effect<
  { readonly report: typeof Report.Type; readonly content: string },
  Failure
> = intervene({ dryRun: false, decision: "approved" }).pipe(
  Effect.provide(NodeFileSystem.layer)
)
```

---

# Delegation trellis
https://smithers.sh/docs/examples/33-delegation-trellis/

Execute a model-authored plan across durable flow rounds.

`Delegate` calls the author action and hands its plan to `RunPlan` as the next round's payload. `RunPlan` can then validate that concrete plan and build its leaves into a `Node.all` join.

The handoff makes runtime-generated work available as input to a subsequent planning round. See the patterns API reference for `Trellis.validate` and `Trellis.leaves`.

## Run the test

The complete program is [`examples/src/33-delegation-trellis.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/33-delegation-trellis.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/33-delegation-trellis.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/patterns`](/docs/reference/api/patterns/) · [`@smthrs/plan`](/docs/reference/api/plan/)

## Source

```ts title="examples/src/33-delegation-trellis.ts"
import { Action, Flow, Graph, Interpreter } from "@smthrs/flow"
import { Trellis } from "@smthrs/patterns"
import { Node } from "@smthrs/plan"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** The bounds this delegation admits a plan under. */
export const envelope: Trellis.Envelope = { fuel: 4, depth: 3, fanout: 3 }

/**
 * The step that authors a plan. A real one calls a model with
 * `Trellis.Plan` as its output schema; this one is scripted so the example
 * runs offline and the assertions stay exact.
 */
export const AuthorPlan = Action.make("examples/AuthorPlan", {
  payload: { goal: Schema.String },
  success: Trellis.Plan
})

/** The step one leaf of the plan turns into. */
export const DelegateWork = Action.make("examples/DelegateWork", {
  payload: { goal: Schema.String, path: Schema.String },
  success: Schema.String
})

/**
 * Round two: the plan arrives as data, so the body reads it.
 *
 * `Trellis.validate` runs here rather than in the author round because this is
 * the round that would spend the fuel. A refused plan settles with the code and
 * the plan path that names the fault, and no leaf step is ever dispatched.
 */
export const RunPlan = Flow.make("examples/RunPlan", {
  payload: { goal: Schema.String, plan: Trellis.Plan },
  success: Schema.Array(Schema.String),
  body: ({ goal, plan }: { readonly goal: string; readonly plan: Trellis.Plan }) => {
    const refusals = Trellis.validate(plan, envelope)
    const refused = refusals[0]
    if (refused !== undefined) return Node.succeed([`refused:${refused.code}:${refused.path}`])

    const leaves = Trellis.leaves(plan)
    const calls: Record<string, Node.Node<string, never, Action.Requirement<"examples/DelegateWork">>> = {}
    leaves.forEach((leaf, index) => {
      calls[`leaf-${index}`] = DelegateWork.call({ goal: leaf.goal, path: leaf.path })
    })

    // The join is built from the plan's own leaf list, so the graph has exactly
    // one node per leaf and the outputs come back in plan order.
    return Node.all(calls).pipe(
      Node.map(Node.capture({ count: leaves.length }, (results: Readonly<Record<string, string>>) =>
        leaves.map((_, index) => results[`leaf-${index}`] as string)))
    )
  }
})

/**
 * Round one: author a plan, then hand off.
 *
 * The body never looks inside the plan. It cannot: at build time the author's
 * result is a placeholder the engine substitutes later. Handing it to the next
 * round is what turns it into a value a body can read.
 */
export const Delegate = Flow.make("examples/Delegate", {
  payload: { goal: Schema.String },
  success: Schema.Array(Schema.String),
  body: ({ goal }: { readonly goal: string }) =>
    AuthorPlan.call({ goal }).pipe(
      Node.bindPlanned(Node.capture({ goal }, (plan) => RunPlan.to({ goal, plan })))
    )
})

/** What `main` reports back to the test. */
export interface Summary {
  readonly result: ReadonlyArray<string>
  readonly authored: number
  readonly dispatched: ReadonlyArray<string>
  readonly leafNodes: number
}

/** The plan the scripted author returns: one leaf, then two run together. */
export const plan: Trellis.Plan = {
  sequence: [
    { agent: { goal: "outline" } },
    { parallel: [{ agent: { goal: "draft" } }, { agent: { goal: "review" } }] }
  ]
}

/**
 * Counts the steps round two's graph actually contains for a plan.
 *
 * `RunPlan` calls exactly one action, so every `ActionCall` in its graph is a
 * leaf of the plan. The count is read off the built graph rather than off the
 * plan, which is what makes it evidence that the plan drove the topology and
 * not a shape written by hand.
 */
export const leafNodesFor = (authored: Trellis.Plan): number =>
  Graph.nodes(Graph.build(RunPlan, { goal: "ship the release notes", plan: authored }))
    .filter((node) => node.kind === "ActionCall").length

/** Runs the two rounds over one SQLite file and reports what each step did. */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    let authored = 0
    const dispatched: Array<string> = []

    const authorPlan = AuthorPlan.toLayer(() =>
      Effect.sync(() => {
        authored += 1
        return plan
      })
    )

    const delegateWork = DelegateWork.toLayer(({ goal, path }) =>
      Effect.sync(() => {
        dispatched.push(`${goal}@${path}`)
        return goal.toUpperCase()
      })
    )

    const result = yield* Effect.scoped(
      Delegate.execute({ goal: "ship the release notes" }, { executionId: "delegate-1" }).pipe(
        Effect.provide(
          Layer.mergeAll(
            authorPlan,
            delegateWork,
            Interpreter.layer(Delegate),
            Interpreter.layer(RunPlan)
          ).pipe(
            Layer.provideMerge(Action.layerImplementations),
            Layer.provideMerge(durableEngine(filename, "delegation-worker"))
          )
        )
      )
    )

    return { result, authored, dispatched, leafNodes: leafNodesFor(plan) }
  }).pipe(Effect.orDie)
```

---

# Human task
https://smithers.sh/docs/examples/34-human-task/

Reject an invalid human answer and ask again after an engine restart.

The first drive parks on an approval token. The second supplies an invalid answer, records the refusal, and parks on a new attempt's token. The third answers that token and completes the run.

The active attempt is read from persistent waiting state. The example does not rely on a process-local counter to decide which request to answer.

## Run the test

The complete program is [`examples/src/34-human-task.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/34-human-task.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/34-human-task.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/run-store`](/docs/reference/api/run-store/)

## Source

```ts title="examples/src/34-human-task.ts"
import * as DurableEngineState from "@smthrs/engine-store/DurableEngineState"
import { Action, DurableDeferred, HumanTask, Interpreter } from "@smthrs/flow"
import * as Flow from "@smthrs/flow/Flow"
import * as Journal from "@smthrs/journal/Journal"
import type * as JournalEvent from "@smthrs/journal/JournalEvent"
import * as AttemptStore from "@smthrs/run-store/AttemptStore"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** The question, and the run that is blocked on it. */
export const Release = Flow.make("examples/Release", {
  payload: { build: Schema.String },
  success: Schema.Json,
  error: HumanTask.HumanTaskFailed,
  body: ({ build }) =>
    HumanTask.action.call({
      name: "release",
      kind: "confirm",
      prompt: `Ship ${build}?`,
      maxAttempts: 3
    })
})

/** The wait point one attempt at answering `Release` resolves through. */
export const tokenFor = (executionId: string, attempt: number): DurableDeferred.Token =>
  DurableDeferred.tokenFromExecutionId(HumanTask.deferred("release", attempt), {
    flow: Release,
    executionId
  })

/**
 * The attempt the ENGINE says the question is open on.
 *
 * A suspended run has a waiting row, and a human task declares its own park, so
 * the row carries `approval` and the token of the attempt that is waiting. The
 * token is opaque, so the attempt is recovered by asking which attempt's wait
 * point that token addresses.
 */
const parkedAttempt = (
  executionId: string,
  attempts: number
): Effect.Effect<number, never, DurableEngineState.DurableEngineState> =>
  Effect.gen(function*() {
    const state = yield* DurableEngineState.DurableEngineState
    const row = yield* state.waiting(executionId)
    if (Option.isNone(row)) return yield* Effect.die(`the run "${executionId}" is not parked`)
    const { reason, token } = row.value
    if (reason !== "approval" || token === null) {
      return yield* Effect.die(`the run "${executionId}" parked under ${reason} rather than on an answer`)
    }
    for (let attempt = 1; attempt <= attempts; attempt++) {
      if (tokenFor(executionId, attempt) === token) return attempt
    }
    return yield* Effect.die(`the run "${executionId}" parked on a token no attempt of its question owns`)
  })

/** What a recorded refusal looks like when it is read back off its step. */
const Refusal = Schema.Struct({
  task: Schema.String,
  attempt: Schema.Number,
  reason: Schema.String
})

/**
 * The refusals the run RECORDED, read out of durable state.
 *
 * A refusal is a sealed step, so it has an attempt row like every other step,
 * and the journal names the steps a run took. Reading the two together is what
 * an operator does to answer "why was that answer sent back": the step's
 * outcome carries the task, the attempt it judged, and the reason.
 */
const refusalsRecorded = (
  executionId: string
): Effect.Effect<
  ReadonlyArray<typeof Refusal.Type>,
  Journal.JournalError | AttemptStore.AttemptStoreError,
  Journal.Journal | AttemptStore.AttemptStore
> =>
  Effect.gen(function*() {
    const journal = yield* Journal.Journal
    yield* journal.flush
    const page = yield* journal.entries({ runId: executionId as JournalEvent.RunId, limit: 500 })
    const attempts = yield* AttemptStore.AttemptStore
    const steps = new Set<string>()
    for (const entry of page.entries) {
      if (entry.eventType !== "flows.engine.attempt-started") continue
      const digest = (entry.payload as { readonly stepKeyDigest?: unknown }).stepKeyDigest
      if (typeof digest === "string") steps.add(digest)
    }
    const refusals: Array<typeof Refusal.Type> = []
    for (const stepKeyDigest of steps) {
      const row = yield* attempts.get({ runId: executionId, stepKeyDigest, attempt: 1 })
      if (Option.isNone(row) || row.value.outcome === undefined) continue
      const refusal = Schema.decodeUnknownExit(Refusal)(row.value.outcome)
      if (Exit.isSuccess(refusal)) refusals.push(refusal.value)
    }
    return refusals.sort((left, right) => left.attempt - right.attempt)
  })

export interface Summary {
  /** The value the run completed with. */
  readonly result: unknown
  /** The attempt the engine's waiting row named, once for each park. */
  readonly parkedOn: ReadonlyArray<number>
  /** The refusals the engine recorded, read back off their steps. */
  readonly refusals: ReadonlyArray<typeof Refusal.Type>
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const executionId = "release-1"
    const engine = (hostId: string) =>
      Layer.mergeAll(HumanTask.layer, Interpreter.layer(Release)).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(filename, hostId))
      )
    const parkedOn: Array<number> = []

    // Phase one: the question is asked and the run parks, unanswered.
    yield* Effect.scoped(
      Effect.gen(function*() {
        yield* Release.execute({ build: "v1.4.0" }, { executionId, discard: true })
        parkedOn.push(yield* parkedAttempt(executionId, 3))
      }).pipe(Effect.provide(engine("worker-a")))
    )

    // Phase two: a fresh process answers attempt one with prose. A confirm
    // cannot accept it, so the run refuses it and parks on attempt two.
    const refusals = yield* Effect.scoped(
      Effect.gen(function*() {
        yield* HumanTask.answer({ token: tokenFor(executionId, 1), value: "yes, ship it" })
        yield* Release.execute({ build: "v1.4.0" }, { executionId, discard: true })
        parkedOn.push(yield* parkedAttempt(executionId, 3))
        return yield* refusalsRecorded(executionId)
      }).pipe(Effect.provide(engine("worker-b")))
    )

    // Phase three: a third process answers the attempt the engine parked on.
    const result = yield* Effect.scoped(
      Effect.gen(function*() {
        yield* HumanTask.answer({ token: tokenFor(executionId, parkedOn[1]!), value: true })
        return yield* Release.execute({ build: "v1.4.0" }, { executionId })
      }).pipe(Effect.provide(engine("worker-c")))
    )

    return { result, parkedOn, refusals }
  }).pipe(Effect.orDie)
```

---

# Poll
https://smithers.sh/docs/examples/34-poll/

Resume a poll after stopping the engine during its durable timer.

Each `Poll.make` round performs one check. The first drive parks between attempts; a fresh engine opens the same SQLite state after the deadline and continues.

The returned check count verifies that the resumed run does not repeat the recorded first attempt.

## Run the test

The complete program is [`examples/src/34-poll.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/34-poll.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/34-poll.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/)

## Source

```ts title="examples/src/34-poll.ts"
import { Action, Interpreter, Poll, Sleep } from "@smthrs/flow"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** The check the poll runs: a deployment that goes live on the third look. */
export const Status = Action.make("examples/Status", {
  payload: { id: Schema.String, attempt: Schema.Number },
  success: Poll.CheckResult(Schema.String)
})

/** The poll: one attempt per round, 120 ms of durable timer between them. */
export const Deployment = Poll.make("examples/Deployment", {
  input: { id: Schema.String },
  result: Schema.String,
  intervalMs: 120,
  backoff: "fixed",
  maxAttempts: 5,
  onTimeout: "fail",
  check: ({ attempt, id }) => Status.call({ attempt, id })
})

export interface Summary {
  /** The output of the check that satisfied the poll. */
  readonly result: string
  /** The attempt number of every check dispatch, in order. */
  readonly checks: ReadonlyArray<number>
  /** The dispatches the first engine made before it was dropped. */
  readonly checksBeforeRestart: ReadonlyArray<number>
}

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const checks: Array<number> = []
    const status = Status.toLayer(({ attempt }) =>
      Effect.sync(() => {
        checks.push(attempt)
        return { satisfied: attempt >= 3, output: `live:${attempt}` }
      })
    )
    const engine = (hostId: string) =>
      // `Sleep.layer` is not optional here: the wait between attempts is an
      // ordinary `system/sleep` node, so a composition without it has a plan
      // node no implementation answers.
      Layer.mergeAll(status, Poll.layer, Sleep.layer, Interpreter.layer(Deployment)).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(filename, hostId))
      )

    // Phase one: the first attempt runs and the round parks on its timer.
    yield* Effect.scoped(
      Deployment.execute({ id: "web" }, { executionId: "deploy-1", discard: true }).pipe(
        Effect.provide(engine("worker-a"))
      )
    )

    const checksBeforeRestart = [...checks]

    // The process is gone while the timer is still pending. Wait past it, so a
    // fresh engine finds the clock due rather than arming a second one.
    yield* Effect.sleep(300)

    // Phase two: a fresh engine picks the lineage up and finishes the poll.
    const result = yield* Effect.scoped(
      Deployment.execute({ id: "web" }, { executionId: "deploy-1" }).pipe(
        Effect.provide(engine("worker-b"))
      )
    )

    return { result, checks, checksBeforeRestart }
  }).pipe(Effect.orDie)
```

---

# Remote cache
https://smithers.sh/docs/examples/35-remote-cache/

Share a sealed step result between engines with separate databases.

Both engines compose a local cache with the same HTTP action-cache tier. Deferred publication keeps the remote request outside the local state transaction. A refused publication leaves the run successful and records an unpublished provenance result.

The example server uses loopback HTTP for the action-cache protocol. It does not compose the remote artifact tier, which requires HTTPS.

## Run the test

The complete program is [`examples/src/35-remote-cache.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/35-remote-cache.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/35-remote-cache.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/step-cache`](/docs/reference/api/step-cache/)

## Source

```ts title="examples/src/35-remote-cache.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import { CacheSync, EngineStore, StepBoundary, WorkspaceSandbox } from "@smthrs/engine-store"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import { Journal, type JournalEvent } from "@smthrs/journal"
import { Jj } from "@smthrs/kernel"
import * as CacheStore from "@smthrs/step-cache/CacheStore"
import * as CombinedCacheStore from "@smthrs/step-cache/CombinedCacheStore"
import * as RemoteCacheStore from "@smthrs/step-cache/RemoteCacheStore"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
import { createServer, type Server } from "node:http"
import { stubJj } from "./durable-layer.ts"

/** What one run of the example observed. */
export interface Summary {
  /** The value each engine answered with. */
  readonly results: readonly [string, string]
  /** How many times the sealed body actually executed across both engines. */
  readonly executions: number
  /** The `GET /ac/{keyDigest}` requests the shared tier served. */
  readonly acReads: number
  /** The `PUT /ac/{keyDigest}` requests the shared tier accepted. */
  readonly acWrites: number
  /** The `unpublished` cache-provenance records the second engine journalled. */
  readonly unpublished: ReadonlyArray<string>
}

/** The recorded entries the in-process action cache holds, keyed by digest. */
interface SharedTier {
  readonly url: string
  readonly reads: () => number
  readonly writes: () => number
  readonly close: () => Promise<void>
}

/**
 * A real HTTP action cache: `GET /ac/{key}` answers a stored entry or 404,
 * `PUT /ac/{key}` records one. `refusePut` makes every write answer 503, which
 * is how the third scenario reproduces an unreachable shared tier.
 */
const startSharedTier = (refusePut: boolean): Promise<SharedTier> => {
  const entries = new Map<string, string>()
  let reads = 0
  let writes = 0
  const server: Server = createServer((request, response) => {
    const key = decodeURIComponent((request.url ?? "").replace(/^\/ac\//, ""))
    if (request.method === "GET") {
      reads++
      const entry = entries.get(key)
      if (entry === undefined) {
        response.writeHead(404).end()
        return
      }
      response.writeHead(200, { "content-type": "application/json" }).end(entry)
      return
    }
    if (request.method === "PUT") {
      if (refusePut) {
        request.resume()
        response.writeHead(503).end()
        return
      }
      const chunks: Array<Buffer> = []
      request.on("data", (chunk: Buffer) => chunks.push(chunk))
      request.on("end", () => {
        writes++
        entries.set(key, Buffer.concat(chunks).toString("utf8"))
        response.writeHead(201).end()
      })
      return
    }
    response.writeHead(405).end()
  })
  return new Promise((resolve) => {
    server.listen(0, "127.0.0.1", () => {
      const address = server.address()
      const port = typeof address === "object" && address !== null ? address.port : 0
      resolve({
        url: `http://127.0.0.1:${port}`,
        reads: () => reads,
        writes: () => writes,
        close: () =>
          new Promise((closed) => {
            server.closeAllConnections()
            server.close(() => closed())
          })
      })
    })
  })
}

/** The declared atom the flow's body names. */
export const Bundle = Action.make("examples/RemoteBundle", {
  payload: { target: Schema.String },
  success: Schema.String
})

export const Build = Flow.make("examples/RemoteBuild", {
  payload: { target: Schema.String },
  success: Schema.String,
  body: (payload) => Bundle.call(payload)
})

/**
 * The sealed step whose result travels.
 *
 * Two declarations make it shareable, and a step missing either stays local:
 *
 * - `idempotencyKey` gives the step an identity another machine can derive.
 *   Without one the engine falls back to a run-local invocation key.
 * - `metadata` declares a HARD file boundary. Only a hermetic step is
 *   cacheable across runs, and the boundary is what the host measures the
 *   execution against to decide whether the claim held. An empty read set and
 *   an empty write set is the honest declaration for a body that touches no
 *   files at all.
 */
export const Compile = (onExecute: () => void) =>
  Action.make({
    name: "examples/RemoteCompile",
    success: Schema.String,
    tier: "sealed",
    idempotencyKey: "examples/remote-compile/v1",
    metadata: { readSet: [], writeSet: [], boundaryMode: "hard" },
    execute: Effect.sync(() => {
      onExecute()
      return "dist/server.js"
    })
  })

/**
 * One durable engine whose step cache reads through, and publishes to, the
 * shared tier at `endpoint`.
 *
 * The composition differs from `durable-layer.ts` is two layers: a `CacheStore` built
 * from the SQL local tier plus a `RemoteCacheStore`, and the `CacheSync` seam
 * the engine publishes through once its write transaction has committed. Both
 * are merged ABOVE the storage layer, so they shadow the local-only
 * `CacheStore` that `NodeRuntime.storage` provides.
 */
const engineLayer = (filename: string, hostId: string, endpoint: string) => {
  const remote = RemoteCacheStore.make({ endpoint })
  const shared = Layer.mergeAll(
    Layer.effect(CacheStore.CacheStore)(
      Effect.gen(function*() {
        const local = yield* CacheStore.make
        return CombinedCacheStore.make({
          local,
          remote: yield* remote,
          publication: "deferred"
        })
      })
    ),
    CacheSync.layer(remote)
  )
  const execution = Layer.mergeAll(
    StepBoundary.layer,
    WorkspaceSandbox.layerFileSystem(),
    shared
  ).pipe(Layer.provideMerge(NodeRuntime.storage(filename)))
  return EngineStore.layer({
    owner: { hostId },
    journalSource: `${hostId}-engine`
  }).pipe(
    Layer.provideMerge(execution),
    Layer.provideMerge(
      Layer.mergeAll(
        stubJj,
        FetchHttpClient.layer,
        // The third declaration a shareable step needs, and the one that lives
        // on the COMPOSITION rather than the action: a complete cache
        // environment. Without it the engine refuses to claim any result is
        // reusable and scopes every key to its own run, so two engines derive
        // two digests and neither ever finds the other's row. It belongs
        // BENEATH the engine, where the dispatch reads it, not only where the
        // caller executes the flow. Declare it only when the composition can
        // name every semantic layer and capability; here there are none of
        // either.
        Action.layerCacheEnvironment({ layers: [], capabilities: {} })
      )
    ),
    Layer.provideMerge(NodeCrypto.layer),
    Layer.provideMerge(NodeFileSystem.layer)
  )
}

const runOn = (
  filename: string,
  hostId: string,
  endpoint: string,
  executionId: string,
  onExecute: () => void
) =>
  Effect.gen(function*() {
    const result = yield* Build.execute({ target: "server" }, { executionId })
    const journal = yield* Journal.Journal
    yield* journal.flush
    const page = yield* journal.entries({ runId: executionId as JournalEvent.RunId, limit: 200 })
    const unpublished = page.entries
      .filter((entry) => entry.eventType === "flows.engine.cache-provenance")
      .map((entry) => entry.payload as { readonly action?: string })
      .filter((payload) => payload.action === "unpublished")
      .map(() => executionId)
    return { result, unpublished }
  }).pipe(
    Effect.provide(
      Layer.mergeAll(
        Bundle.toLayer(({ target }) =>
          Effect.map(Compile(onExecute), (artifact) => `${artifact}?target=${target}`)
        ),
        Interpreter.layer(Build)
      ).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(engineLayer(filename, hostId, endpoint))
      )
    ),
    Effect.scoped,
    Effect.orDie
  )

/**
 * Runs the same flow on two engines over two database files, against one shared
 * action cache.
 *
 * With `refusePublish`, the shared tier answers every write 503: both engines
 * then execute the body, both succeed, and both journal the refusal.
 */
export const main = async (options: {
  readonly first: string
  readonly second: string
  readonly refusePublish?: boolean
}): Promise<Summary> => {
  const tier = await startSharedTier(options.refusePublish === true)
  let executions = 0
  const count = () => {
    executions++
  }
  try {
    const first = await Effect.runPromise(runOn(options.first, "engine-a", tier.url, "remote-a", count))
    const second = await Effect.runPromise(runOn(options.second, "engine-b", tier.url, "remote-b", count))
    return {
      results: [first.result, second.result],
      executions,
      acReads: tier.reads(),
      acWrites: tier.writes(),
      unpublished: [...first.unpublished, ...second.unpublished]
    }
  } finally {
    await tier.close()
  }
}
```

---

# Detached children
https://smithers.sh/docs/examples/36-detached-children/

Let a child outlive its parent and collect its output after a restart.

The first engine spawns a detached child and completes the parent. A fresh engine opens the same SQLite state and reads the child's result. The child's recorded detach policy keeps it alive when the parent settles.

This differs from an attached child, whose parent-exit policy requests cancellation when the parent exits.

## Run the test

The complete program is [`examples/src/36-detached-children.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/36-detached-children.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/36-detached-children.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/registry`](/docs/reference/api/registry/) · [`@smthrs/run-store`](/docs/reference/api/run-store/)

## Source

```ts title="examples/src/36-detached-children.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { ChildFlows, EngineChildren } from "@smthrs/agent"
import * as ControlLive from "@smthrs/control/ControlLive"
import * as SqlControlRuntime from "@smthrs/control/SqlControlRuntime"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { NotificationQueue } from "@smthrs/notifications"
import { Registry } from "@smthrs/registry"
import { RunStore } from "@smthrs/run-store"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** The work the detached child does. */
export const Summarize = Action.make("examples/Summarize", {
  payload: { document: Schema.String },
  success: Schema.String
})

/** The child, as an ordinary flow. Nothing marks it as a subagent. */
export const Digest = Flow.make("examples/Digest", {
  payload: { document: Schema.String },
  success: Schema.String,
  body: (payload) => Summarize.call(payload)
})

/** The parent's one step: start the child and answer with its id. */
export const StartDigest = Action.make("examples/StartDigest", {
  payload: { document: Schema.String },
  success: Schema.String
})

export const Triage = Flow.make("examples/Triage", {
  payload: { document: Schema.String },
  success: Schema.String,
  body: (payload) => StartDigest.call(payload)
})

/**
 * What the two phases observed.
 *
 * `parentStatus` and `childCancelRequested` are read straight off the run rows
 * after phase one, because the claim being made is about durable state and not
 * about what either engine remembers.
 */
export interface Summary {
  readonly child: string
  readonly parentStatus: string
  readonly childCancelRequested: boolean
  readonly output: string
}

/**
 * The control plane the child port steers through, over the engine's database.
 *
 * `EngineChildren` depends on exactly three services (the flow runtime, the
 * run store, and the control plane) so a host wires the control plane once
 * and gets `agent/send` with it. This example only spawns and collects, but
 * the dependency is real and is wired honestly rather than stubbed.
 */
const controlPlane = Layer.provide(ControlLive.layer, [
  SqlControlRuntime.layer(),
  Registry.layerNoop(),
  NotificationQueue.layer
]).pipe(Layer.orDie)

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const summarize = ({ document }: { readonly document: string }) => Effect.succeed(`summary of ${document}`)

    const startDigest = ({ document }: { readonly document: string }) =>
      Effect.gen(function*() {
        const children = yield* ChildFlows.Children
        const spawned = yield* children.spawn({
          flow: Digest._tag,
          input: { document },
          label: "digest"
        })
        return spawned.child
      }).pipe(Effect.orDie)

    const engine = (hostId: string) =>
      Layer.mergeAll(
        StartDigest.toLayer(startDigest),
        Summarize.toLayer(summarize),
        Interpreter.layer(Triage),
        Interpreter.layer(Digest)
      ).pipe(
        Layer.provideMerge(Action.layerImplementations),
        // The port names the flows a child may run. A `spawn` of anything else
        // is `ChildError { code: "not_found" }`, which the caller can see.
        Layer.provideMerge(EngineChildren.layer({ flows: [Digest] })),
        Layer.provideMerge(controlPlane),
        Layer.provideMerge(durableEngine(filename, hostId)),
        Layer.provideMerge(NodeCrypto.layer)
      )

    // Phase one: the parent spawns the child and finishes. The child keeps
    // going, because `spawn` discarded its result and the engine recorded
    // `onParentExit: "detach"` on the child's own row.
    const phaseOne = yield* Effect.scoped(
      Effect.gen(function*() {
        const child = yield* Triage.execute({ document: "rfc" }, { executionId: "triage-1" })
        const store = yield* RunStore.RunStore
        const parentRow = yield* store.get("triage-1")
        const childRow = yield* store.get(child)
        return {
          child,
          parentStatus: parentRow.status as string,
          childCancelRequested: childRow.cancelRequestedAtMs !== null
        }
      }).pipe(Effect.provide(engine("worker-a")))
    )

    // Phase two: a fresh engine, a different owner, no fiber and no map from
    // phase one. What it answers with came out of the run store.
    const output = yield* Effect.scoped(
      Effect.gen(function*() {
        const children = yield* ChildFlows.Children
        const collected = yield* children.await({ child: phaseOne.child })
        return collected.output
      }).pipe(Effect.provide(engine("worker-b")))
    )

    return { ...phaseOne, output }
  }).pipe(Effect.orDie)
```

---

# Host containment
https://smithers.sh/docs/examples/37-host-containment/

Recover process cleanup after a host is killed with SIGKILL.

The host records each spawned process group in the durable `ProcessLedger`. A later incarnation of the same host ID inspects those records, reaps groups whose owner is gone, and journals the result.

The companion host program runs in a separate process so the example can kill it without running its finalizers. This exercises recovery that a graceful shutdown would not test.

## Run the test

The complete program is [`examples/src/37-host-containment.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/37-host-containment.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/37-host-containment.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flows`](/docs/reference/api/flows/)

## Source

```ts title="examples/src/37-host-containment.ts"
import { Journal, Kernel } from "@smthrs/flows"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import { spawn } from "node:child_process"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"

/**
 * The host id both incarnations share. It is what makes the records theirs.
 * @since 1.0.0-rc.0
 * @category constants
 */
export const hostId = "examples/host-containment"

const hostProgram = join(dirname(fileURLToPath(import.meta.url)), "37-host-containment-host.ts")

/**
 * Observations from the killed host and its replacement.
 * @since 1.0.0-rc.0
 * @category models
 */
export interface Summary {
  /** Diagnostics from the host before it was killed, including Node runtime warnings. */
  readonly hostStderr: string
  /** The process group the killed host started. */
  readonly pgid: number
  /** Whether that group survived the host, which is the problem to solve. */
  readonly orphaned: boolean
  /** Whether it survived the next incarnation, which is the answer. */
  readonly survivedTheReaper: boolean
  /** What the host's own journal run records, in order. */
  readonly hostEvents: ReadonlyArray<string>
}

const groupIsAlive = (pgid: number): boolean => {
  try {
    process.kill(-pgid, 0)
    return true
  } catch {
    return false
  }
}

/**
 * Runs the host until it reports the group it started, kills it, and waits for
 * the operating system to reap it so its pid genuinely reads as gone.
 */
const killHost = (filename: string): Promise<{ readonly pgid: number; readonly hostStderr: string }> =>
  new Promise((resolve, reject) => {
    const child = spawn(process.execPath, [hostProgram, filename, hostId], { stdio: ["ignore", "pipe", "pipe"] })
    let announced = ""
    let stderr = ""
    let pgid: number | undefined
    child.stdout.setEncoding("utf8")
    child.stderr.setEncoding("utf8")
    child.stdout.on("data", (chunk: string) => {
      announced += chunk
      if (pgid === undefined && announced.includes("\n")) {
        pgid = Number(announced.split("\n")[0])
        // No handler runs for this, which is the whole point.
        child.kill("SIGKILL")
      }
    })
    child.stderr.on("data", (chunk: string) => {
      stderr += chunk
    })
    child.on("error", reject)
    child.on("close", () => {
      if (pgid === undefined) reject(new Error(`the host exited before announcing a group: ${stderr}`))
      else resolve({ pgid, hostStderr: stderr })
    })
  })

/** Waits until `pgid` is gone, or gives up after `budgetMs`. */
const waitForGroupExit = (pgid: number, budgetMs: number): Promise<boolean> =>
  new Promise((resolve) => {
    const deadline = Date.now() + budgetMs
    const poll = () => {
      if (!groupIsAlive(pgid)) return resolve(true)
      if (Date.now() > deadline) return resolve(false)
      setTimeout(poll, 25)
    }
    poll()
  })

/**
 * Starts, kills, and replaces a host over the same durable journal.
 * @since 1.0.0-rc.0
 * @category examples
 */
export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    // ----------------------------------------------------------------- crash
    const { pgid, hostStderr } = yield* Effect.promise(() => killHost(filename))
    const orphaned = groupIsAlive(pgid)

    // --------------------------------------------------------------- restart
    // A new host, given nothing about the dead one but its id. Building the
    // layer IS the sweep: the reaper runs while the host stands up, so by the
    // time this body runs the decision has already been made and journaled.
    const hostEvents = yield* Effect.gen(function*() {
      const journal = yield* Journal.Journal.Journal
      const page = yield* journal.entries({
        runId: Kernel.ProcessLedger.hostRunId(hostId),
        limit: 16
      })
      return page.entries.map((entry) => entry.eventType)
    }).pipe(
      Effect.provide(
        NodeRuntime.layerHost(
          { filename, workspaceRoot: dirname(filename), owner: { hostId }, signals: [] },
          Layer.empty
        )
      ),
      Effect.orDie,
      Effect.scoped
    )

    const survivedTheReaper = !(yield* Effect.promise(() => waitForGroupExit(pgid, 10_000)))
    return { pgid, hostStderr, orphaned, survivedTheReaper, hostEvents }
  })
```

## Companion: 37-host-containment-host.ts

The host program `37-host-containment.ts` kills.

It is a separate process because the example is about what survives a host that never got to run a finalizer, and there is no way to produce that state from inside the process that has to observe it.

Usage: `node 37-host-containment-host.ts <sqlite file> <hostId>`. It prints the process id of the group it started, then waits to be killed. Startup failures print their cause to stderr and exit with status 1.

```ts title="examples/src/37-host-containment-host.ts"
import { Action, Capability, Flow, Interpreter } from "@smthrs/flows"
import * as NodeRuntime from "@smthrs/flows/NodeRuntime"
import * as Cause from "effect/Cause"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { dirname } from "node:path"

const filename = process.argv[2]
const hostId = process.argv[3]
if (filename === undefined || hostId === undefined) {
  throw new Error("usage: 37-host-containment-host.ts <sqlite file> <hostId>")
}

/** Starts a two-process tree that outlives anything short of a group signal. */
const Spawn = Action.make({
  name: "examples/host-containment/spawn",
  success: Schema.String,
  tier: "sealed",
  idempotencyKey: "examples/host-containment/spawn/v1",
  execute: Effect.gen(function*() {
    const spawner = yield* ChildProcessSpawner
    const handle = yield* Effect.orDie(spawner.spawn(ChildProcess.make("sh", ["-c", "sleep 300 & sleep 300"])))
    // Printed only after the spawn returned, and the spawn returns only after
    // the ledger has durably recorded it. Whoever reads this line may kill this
    // process immediately.
    process.stdout.write(`${handle.pid as number}\n`)
    yield* Effect.orDie(handle.exitCode)
    return "woke"
  })
})

const Start = Action.make("examples/host-containment/start", {
  payload: { what: Schema.String },
  success: Schema.String
})

const Containment = Flow.make("examples/host-containment", {
  payload: { what: Schema.String },
  success: Schema.String,
  body: (payload) => Start.call(payload)
})

const flows = Interpreter.layer(Containment).pipe(
  Layer.provideMerge(Start.toLayer(() => Spawn)),
  Layer.provideMerge(Action.layerImplementations)
)

const exit = await Effect.runPromise(
  Effect.exit(
    Containment.execute({ what: "spawn" }, { executionId: "host-containment" }).pipe(
      Effect.provide(
        NodeRuntime.layerHost(
          {
            filename,
            workspaceRoot: dirname(filename),
            owner: { hostId },
            signals: [],
            rules: [
              new Capability.Permission.Rule({
                effect: "allow",
                pattern: new Capability.Capability.CapabilityPattern({ action: "proc:spawn", resource: "*" })
              })
            ]
          },
          flows
        )
      ),
      Effect.scoped
    )
  )
)

if (Exit.isFailure(exit)) {
  process.stderr.write(`${Cause.pretty(exit.cause)}\n`)
  process.exitCode = 1
}
```

---

# Monitor and alert
https://smithers.sh/docs/examples/38-monitor-and-alert/

Observe parked runs and test delayed alert delivery.

One run receives its answer and finishes. Another remains parked while a monitor reads its state, records a diagnosis, and attempts opt-in recovery. The alert policy admits a notification after the unhealthy condition lasts long enough.

The example compares a positive alert delay with a zero-delay policy. A monitor cannot infer whether an unanswered wait will eventually receive a response, so automatic recovery remains an explicit choice.

## Run the test

The complete program is [`examples/src/38-monitor-and-alert.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/38-monitor-and-alert.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/38-monitor-and-alert.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/control`](/docs/reference/api/control/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/notifications`](/docs/reference/api/notifications/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/38-monitor-and-alert.ts"
import { Control, ControlLive, Monitor, SqlControlRuntime } from "@smthrs/control"
import { Action, DurableDeferred, Flow, Interpreter, WaitFor } from "@smthrs/flow"
import { Alerts, NotificationQueue } from "@smthrs/notifications"
import { Registry } from "@smthrs/registry"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { durableEngine } from "./durable-layer.ts"

/** What the supervision loop concluded. */
export interface Summary {
  /** What the run whose approval arrived returned. */
  readonly answered: unknown
  /** The status the control plane reported for the unanswered run. */
  readonly parked: string
  /** What the parked run was waiting for, as the engine recorded it. */
  readonly waitingFor: string | undefined
  /** The health of each monitor beat, in order. */
  readonly beats: ReadonlyArray<Monitor.Health>
  /** The remedy the monitor applied, once it had a receipt for it. */
  readonly healed: Monitor.Remedy | undefined
  /** Alerts raised under the production delay. */
  readonly quiet: number
  /** The conditions the impatient policy paged about. */
  readonly paged: ReadonlyArray<string>
  /** Alerts a second tick raised. Zero: a delivered alert is not re-sent. */
  readonly repaged: number
  /** Coalescing keys pending on the notification queue. */
  readonly pending: ReadonlyArray<string>
}

/** The wait point both runs park on, and the resolver's half of it. */
const approval = WaitFor.deferred("approval")

/**
 * The supervised flow: one step, which waits for an approval.
 *
 * The wait's failure is declared rather than swallowed. `WaitFor` refuses a
 * payload that does not name exactly one reachable wait point, and a body that
 * hid that would turn an addressing mistake into a run that parks forever.
 */
export const Supervised = Flow.make("examples/Supervised", {
  payload: {},
  success: Schema.Json,
  error: WaitFor.WaitForRequestInvalid,
  body: () => WaitFor.action.call({ name: "approval" })
})

/** The run whose approval arrives. */
const answeredRunId = "examples-answered"

/** The run nobody answers, and the one the monitor is pointed at. */
const supervisedRunId = "examples-supervised"

/**
 * The policy: page about a run the monitor called wedged.
 *
 * `wedged-node` is not one of the four conditions `defaultDetectors` covers, so
 * the policy brings its own: the field to read, the value that means the
 * condition holds, and the entries to read it on. Narrowing to the beat keeps
 * the heal record, which names the same health, from re-opening a condition
 * the next beat closed.
 */
const rules = (afterMs: number): Alerts.Policy => ({
  defaults: { severity: "warning", owner: "oncall" },
  rules: { "wedged-node": { afterMs, runbook: "https://runbook/wedged-runs" } },
  detectors: {
    "wedged-node": { field: "health", value: "wedged-node", eventTypes: [Monitor.beatEventType] }
  }
})

/**
 * The action implementation and flow registration the engine needs.
 *
 * Declaration and implementation stay separate exactly as in example 02: the
 * flow is data, and this layer attaches the code on a host that can run it.
 */
const registrations = Layer.mergeAll(
  WaitFor.layer,
  Interpreter.layer(Supervised)
).pipe(Layer.provideMerge(Action.layerImplementations))

/**
 * The control plane, over whatever database is beneath it.
 *
 * No `ControlExecutor` is provided, and that is deliberate rather than a
 * shortcut: this control plane starts nothing. It observes and steers runs the
 * engine owns, which is the case a monitor actually meets, and `ControlLive`
 * treats the acceptance port as optional for exactly that reason.
 */
const controlPlane = ControlLive.layer.pipe(
  Layer.provideMerge(
    Layer.mergeAll(
      SqlControlRuntime.layer({ owner: { hostId: "examples-monitor", pid: 1, nonce: "monitor" } }).pipe(Layer.orDie),
      NotificationQueue.layer,
      Registry.layerNoop()
    )
  )
)

/**
 * The engine and the control plane over ONE database.
 *
 * `Layer.provideMerge` builds what it provides privately, so composing the
 * storage twice would give the monitor its own empty copy of the rows it is
 * supposed to be reading.
 */
const stack = (filename: string) =>
  Layer.merge(controlPlane, registrations).pipe(
    Layer.provideMerge(durableEngine(filename, "examples-monitor"))
  )

/** The token that names one execution's wait point. */
const tokenFor = (executionId: string) =>
  DurableDeferred.tokenFromExecutionId(approval, { flow: Supervised, executionId })

/**
 * Runs the supervision loop over durable runs.
 *
 * @param filename the SQLite file to run against
 * @param productionDelayMs how long a condition must last before a real policy pages
 */
export const main = (
  filename: string,
  productionDelayMs = 900_000
): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const control = yield* Control.Control

    // A run that parks and is then answered. `execute` returns the id while
    // the run is still parked; the engine rebuilds the awaiting frame from the
    // journal when the deferred is completed, and the second execute reads the
    // finished result rather than re-running the body.
    yield* Supervised.execute({}, { executionId: answeredRunId, discard: true })
    yield* DurableDeferred.succeed(approval, { token: tokenFor(answeredRunId), value: { approved: true } })
    const answered = yield* Supervised.execute({}, { executionId: answeredRunId })

    // A run that parks and is not answered. Nothing is driving it now, which
    // is the situation a monitor exists for.
    yield* Supervised.execute({}, { executionId: supervisedRunId, discard: true })
    const listed = yield* control.list({ _tag: "runs", filters: { runId: supervisedRunId } })
    const parked = listed._tag === "runs" ? listed.items[0] : undefined

    // Four beats: three build the stall, the fourth classifies it and resumes.
    // The resume claims the run back onto the control plane, which is where a
    // deployment's own executor picks it up.
    const report = yield* Monitor.run({
      runId: supervisedRunId,
      monitorId: "examples-oncall",
      intervalMs: 0,
      maxChecks: 4,
      stallBeats: 3,
      autoHeal: ["stalled", "wedged-node"]
    })

    // The alert policy reads the beats the monitor just wrote.
    const patient = yield* Effect.provide(
      Effect.flatMap(Alerts.AlertRuntime, (alerts) => alerts.tick(supervisedRunId)),
      Alerts.layer(rules(productionDelayMs)).pipe(Layer.provideMerge(Alerts.layerNoop))
    )
    const impatient = Alerts.layer(rules(0)).pipe(Layer.provideMerge(Alerts.layerNoop))
    const paged = yield* Effect.provide(
      Effect.flatMap(Alerts.AlertRuntime, (alerts) => alerts.tick(supervisedRunId)),
      impatient
    )
    const again = yield* Effect.provide(
      Effect.flatMap(Alerts.AlertRuntime, (alerts) => alerts.tick(supervisedRunId)),
      impatient
    )
    const queue = yield* NotificationQueue.NotificationQueue
    const pending = yield* queue.pending(supervisedRunId)

    return {
      answered,
      parked: parked?.status ?? "unknown",
      waitingFor: parked?.waitingReason,
      beats: report.beats.map((beat) => beat.health),
      healed: report.beats.find((beat) => beat.healed !== undefined)?.healed,
      quiet: patient.delivered.length,
      paged: paged.delivered.map((alert) => alert.condition),
      repaged: again.delivered.length,
      pending: pending.flatMap((notification) =>
        notification._tag === "system-event" && notification.coalescingKey !== undefined
          ? [notification.coalescingKey]
          : []
      )
    }
  }).pipe(Effect.provide(stack(filename)), Effect.scoped, Effect.orDie)
```

---

# Agent policies
https://smithers.sh/docs/examples/39-agent-policies/

Resume a model-backed action after a rate limit and an invalid structured answer.

The scripted provider first returns HTTP 429 with a retry delay, then an answer outside the schema, then valid JSON after correction. A second engine continues from the parked state in the same SQLite file.

The summary checks provider call counts, the recorded park decision, and the structured-output rejection. No provider credentials are needed.

## Run the test

The complete program is [`examples/src/39-agent-policies.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/39-agent-policies.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/39-agent-policies.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/agent`](/docs/reference/api/agent/) · [`@smthrs/engine-store`](/docs/reference/api/engine-store/) · [`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/journal`](/docs/reference/api/journal/) · [`@smthrs/model`](/docs/reference/api/model/) · [`@smthrs/registry`](/docs/reference/api/registry/)

## Source

```ts title="examples/src/39-agent-policies.ts"
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import * as Agent from "@smthrs/agent/Agent"
import * as AgentAction from "@smthrs/agent/AgentAction"
import * as Budget from "@smthrs/agent/Budget"
import * as QuotaPolicy from "@smthrs/agent/QuotaPolicy"
import * as Seat from "@smthrs/agent/Seat"
import * as SeatResolver from "@smthrs/agent/SeatResolver"
import * as DurableEngineState from "@smthrs/engine-store/DurableEngineState"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import { Journal, type JournalEvent } from "@smthrs/journal"
import * as Model from "@smthrs/model/Model"
import { ModelError } from "@smthrs/model/ModelError"
import * as ModelEvent from "@smthrs/model/ModelEvent"
import type * as Route from "@smthrs/model/Route"
import * as Registry from "@smthrs/registry/Registry"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Schedule from "effect/Schedule"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { durableEngine } from "./durable-layer.ts"

/** The declared answer shape. Nothing downstream parses model text. */
const Review = Schema.Struct({
  approved: Schema.Boolean,
  issues: Schema.Array(Schema.String)
})

/**
 * The step. `corrections: 1` says one re-prompt, and the re-prompt repeats the
 * task verbatim with the validation issues appended.
 */
export const Reviewer = AgentAction.make("examples/PolicyReviewer", {
  payload: { diff: Schema.String },
  output: Review,
  seat: "anthropic:claude-sonnet-4-5",
  system: ["You review diffs and report whether they are approvable."],
  prompt: ({ diff }) => `Review this diff:\n${diff}`,
  corrections: 1
})

/** The flow: one step, so the policies are the only thing on show. */
export const ReviewFlow = Flow.make("examples/PolicyReviewFlow", {
  payload: { diff: Schema.String },
  success: Review,
  error: AgentAction.AgentFailure,
  body: ({ diff }) => Reviewer.call({ diff })
})

const prepared: Route.PreparedRequest = {
  routeId: "examples",
  protocolId: "examples",
  method: "POST",
  url: "https://example.invalid/v1/messages",
  publicHeaders: { "content-type": "application/json" },
  body: new TextEncoder().encode("{}"),
  bodyText: "{}"
}

/** A cell that finishes the turn with one literal answer. */
const answering = (output: string): string => `ctx.done(${JSON.stringify(output)})`

/**
 * The refusal a real provider sends when a window is closed: HTTP 429 with the
 * seconds until it reopens. `retryAfterMillis` is what the classifier turns
 * into the wake time.
 */
const refusal = new ModelError({
  code: "rate_limited",
  message: "Too many requests",
  retryAfterMillis: 1_000,
  httpStatus: 429
})

/**
 * The scripted provider. `calls` counts what actually reached it, which is how
 * a replayed step is told apart from a re-issued one.
 */
const scripted = (calls: Array<string>): Model.Model =>
  Model.make({
    stream: () =>
      Stream.suspend(() => {
        calls.push("call")
        if (calls.length === 1) return Stream.fail(refusal)
        const cell = calls.length === 2
          ? answering("Looks fine to me.")
          : answering(`{"approved":true,"issues":[]}`)
        return Stream.fromIterable([
          ModelEvent.ModelEvent.TextStart({ type: "text-start", id: "cell" }),
          ModelEvent.ModelEvent.TextDelta({
            type: "text-delta",
            id: "cell",
            text: "```cell\n" + cell + "\n```"
          }),
          ModelEvent.ModelEvent.TextEnd({ type: "text-end", id: "cell" }),
          ModelEvent.ModelEvent.Usage({ totalTokens: 400 }),
          ModelEvent.ModelEvent.Settle({ type: "settle", stopReason: "stop" })
        ])
      })
  })

/**
 * The composition half. `Host` carries what every model-backed action in this
 * composition shares. The default classifier is the behavior this example is
 * demonstrating, and the written token ceiling is the one the example chose.
 * A host opting out would say `layerUnclassified()` and `layerUnbounded()`.
 */
const policies = (calls: Array<string>) =>
  Layer.mergeAll(
    AgentAction.layerHost({
      registry: Registry.makeNoop({
        list: () => Effect.succeed([]),
        visible: () => Effect.succeed([]),
        getOption: () => Effect.succeed(Option.none())
      }),
      limits: { calls: 8 },
      capabilityEnvelope: [],
      maxFrames: 4,
      // A quota refusal is not a transport hiccup, so the transport ladder is
      // off: the park is the only thing that waits.
      modelRetryPolicy: Schedule.recurs(0)
    }),
    SeatResolver.layer({
      resolve: (id) =>
        Effect.succeed(
          Seat.make({
            id,
            model: scripted(calls),
            route: { prepare: () => Effect.succeed(prepared) },
            contextWindowTokens: 200_000
          })
        )
    }),
    Agent.layer
  ).pipe(
    Layer.provideMerge(
      Layer.mergeAll(
        QuotaPolicy.layerDefault(),
        // A ceiling in `warn`: the run reports what it spends without being
        // stopped, which is how a budget is introduced to a live workflow.
        // Under `fail` the same ceiling would end the step instead.
        Budget.layer({ tokens: { max: 500, onExceeded: "warn" } })
      )
    )
  )

const engine = (filename: string, hostId: string, calls: Array<string>) =>
  Layer.mergeAll(Reviewer.layer, Interpreter.layer(ReviewFlow)).pipe(
    Layer.provideMerge(policies(calls)),
    Layer.provideMerge(Agent.layerDefaults),
    Layer.provideMerge(Action.layerImplementations),
    Layer.provideMerge(durableEngine(filename, hostId)),
    Layer.provideMerge(NodeCrypto.layer)
  )

/** Waits for the parked run to appear in the operator's own view of waits. */
const waitForPark = (state: DurableEngineState.Service) =>
  Effect.gen(function*() {
    for (let poll = 0; poll < 400; poll++) {
      const rows = yield* state.waitingRuns({ reason: "quota" })
      if (rows.length > 0) return rows
      yield* Effect.sleep("10 millis")
    }
    return yield* state.waitingRuns({ reason: "quota" })
  })

/** What the run left behind, read back from its own durable trail. */
export interface Summary {
  readonly review: typeof Review.Type
  readonly providerCalls: number
  readonly callsBeforeTheRestart: number
  readonly wakeAt: number
  readonly parks: number
  readonly corrections: number
  readonly budgetWarnings: number
}

const executionId = "policy-review-1"

export const main = (filename: string): Effect.Effect<Summary> =>
  Effect.gen(function*() {
    const calls: Array<string> = []

    // Phase one: the provider refuses, the run parks under the quota reason,
    // and the engine is then killed while the run is still waiting.
    const waiting = yield* Effect.scoped(
      Effect.gen(function*() {
        const state = yield* DurableEngineState.DurableEngineState
        const running = yield* ReviewFlow.execute({ diff: "-  old\n+  new" }, { executionId }).pipe(
          Effect.forkChild({ startImmediately: true })
        )
        const rows = yield* waitForPark(state)
        yield* Fiber.interrupt(running)
        return rows
      }).pipe(Effect.provide(engine(filename, "worker-a", calls)))
    )
    const callsBeforeTheRestart = calls.length

    // Phase two: a second engine over the same file waits out the deadline the
    // first one recorded, spends the correction, and finishes the run.
    const finished = yield* Effect.scoped(
      Effect.gen(function*() {
        const review = yield* ReviewFlow.execute({ diff: "-  old\n+  new" }, { executionId })
        const journal = yield* Journal.Journal
        yield* journal.flush
        const page = yield* journal.entries({ runId: executionId as JournalEvent.RunId, limit: 500 })
        const count = (eventType: string) => page.entries.filter((entry) => entry.eventType === eventType).length
        return {
          review,
          parks: count(QuotaPolicy.quotaParkedEvent),
          corrections: count(AgentAction.structuredOutputRejectedEvent),
          budgetWarnings: count(Budget.budgetWarningEvent)
        }
      }).pipe(Effect.provide(engine(filename, "worker-b", calls)))
    )

    return {
      ...finished,
      providerCalls: calls.length,
      callsBeforeTheRestart,
      wakeAt: waiting[0]?.wakeAt ?? 0
    }
  }).pipe(Effect.orDie)
```

---

# Sandbox placement
https://smithers.sh/docs/examples/40-sandbox-placement/

Place an action's host operations in a provisioned session.

The engine and journal remain local. `Sandbox.layerHost` supplies filesystem and process services from one session, so a process can read the file written through that session's filesystem.

The example uses a scratch directory as its provisioned environment. Closing the action scope releases the session and removes its workspace; the action's JavaScript still executes in the engine host.

## Run the test

The complete program is [`examples/src/40-sandbox-placement.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/40-sandbox-placement.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/40-sandbox-placement.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/)

## Source

```ts title="examples/src/40-sandbox-placement.ts"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as ProcessLedger from "@smthrs/kernel/ProcessLedger"
import * as NodeHost from "@smthrs/platform-node/NodeHost"
import { DirectorySandbox, Sandbox } from "@smthrs/sandbox"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as ChildProcess from "effect/unstable/process/ChildProcess"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { durableEngine } from "./durable-layer.ts"

/** The bytes the placed action writes and asks the sandboxed process to count. */
export const contents = "the action body ran in its sandbox\n"

/** The declared atom whose implementation is placed on the sandbox machine. */
export const CountBytes = Action.make("examples/SandboxPlacement/CountBytes", {
  payload: { contents: Schema.String },
  success: Schema.Number
})

/** A durable flow whose graph is independent of the action's placement. */
export const SandboxPlacement = Flow.make("examples/SandboxPlacement", {
  payload: { contents: Schema.String },
  success: Schema.Number,
  body: (payload) => CountBytes.call(payload)
})

/** The unchanged host-shaped body that writes a file and executes a process. */
const writeAndCount = ({ contents }: { readonly contents: string }) =>
  Effect.gen(function*() {
    const fs = yield* FileSystem.FileSystem
    const spawner = yield* ChildProcessSpawner
    yield* fs.writeFileString("example-40/placed.txt", contents)
    const printed = yield* spawner.string(ChildProcess.make("wc", ["-c", "example-40/placed.txt"]))
    const count = Number.parseInt(printed.trim(), 10)
    if (!Number.isSafeInteger(count)) {
      return yield* Effect.die(new Error(`wc printed an invalid byte count: ${printed}`))
    }
    return count
  }).pipe(Effect.orDie)

export interface MainOptions {
  /** The local SQLite file that holds the engine and journal. */
  readonly filename: string
  /** The local parent under which the provider provisions its scratch machine. */
  readonly root: string
}

/** Runs the placed action once and closes every acquired layer before resolving. */
export const main = (options: MainOptions): Promise<number> =>
  Effect.runPromise(
    Effect.gen(function*() {
      const fs = yield* FileSystem.FileSystem
      const spawner = yield* ChildProcessSpawner
      const placedHost = Sandbox.layerHost(
        DirectorySandbox.make({ fs, spawner, root: options.root }),
        { session: "examples/sandbox-placement" }
      )
      const stack = Layer.mergeAll(
        CountBytes.toLayer((payload) =>
          writeAndCount(payload).pipe(
            Effect.provide(placedHost),
            Effect.orDie
          )
        ),
        Interpreter.layer(SandboxPlacement)
      ).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(options.filename, "examples-sandbox-placement"))
      )

      return yield* SandboxPlacement.execute(
        { contents },
        { executionId: "sandbox-placement" }
      ).pipe(
        Effect.provide(stack),
        Effect.scoped
      )
    }).pipe(
      Effect.provide(
        NodeHost.layerContained().pipe(
          Layer.provide(ProcessLedger.layerMemory({ hostId: "examples-sandbox-placement", ownerPid: process.pid }))
        )
      ),
      Effect.orDie
    )
  )
```

---

# Sandboxed flow
https://smithers.sh/docs/examples/41-sandboxed-flow/

Run a child flow's code inside a provisioned environment as one parent action.

`SandboxedFlow` bundles the child module and guest runner, starts Node inside the session, and validates the guest's result against the child schema. The parent records this as one durable action.

A second drive over the same SQLite state reuses the recorded result without acquiring another session. `collectDiff` returns the guest-written file as data; applying that change elsewhere remains the caller's decision.

## Run the test

The complete program is [`examples/src/41-sandboxed-flow.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/41-sandboxed-flow.ts). Clone the repository and follow [CONTRIBUTING.md](https://github.com/smithersai/smithers/blob/main/CONTRIBUTING.md) to install and build its packages.

From the repository root, run this example's assertions:

```bash
pnpm --filter @smthrs/examples exec vitest run test/41-sandboxed-flow.test.ts
```

Vitest reports the assertions that passed, failed, or were skipped. Examples that call live providers need the credentials described below; a skipped test does not verify a provider call.

## Related reference

[`@smthrs/flow`](/docs/reference/api/flow/) · [`@smthrs/flows`](/docs/reference/api/flows/) · [`@smthrs/kernel`](/docs/reference/api/kernel/) · [`@smthrs/platform-node`](/docs/reference/api/platform-node/) · [`@smthrs/sandbox`](/docs/reference/api/sandbox/)

## Source

```ts title="examples/src/41-sandboxed-flow.ts"
import { Action, Flow, Interpreter } from "@smthrs/flow"
import * as SandboxedFlow from "@smthrs/flows/SandboxedFlow"
import * as ProcessLedger from "@smthrs/kernel/ProcessLedger"
import * as NodeHost from "@smthrs/platform-node/NodeHost"
import { DirectorySandbox, type Sandbox } from "@smthrs/sandbox"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { durableEngine } from "./durable-layer.ts"
import { Greet, Greeting } from "./sandboxed-child.ts"

/** The parent's action: one sandboxed execution of the child flow. */
export const RunGreet = SandboxedFlow.action(Greet)

/** A durable parent flow whose one step is the sandboxed child. */
export const SandboxedGreeting = Flow.make("examples/SandboxedGreeting", {
  payload: { name: Schema.String },
  success: SandboxedFlow.resultSchema(Greeting),
  error: SandboxedFlow.SandboxedFlowError,
  body: (payload) => RunGreet.call(payload)
})

export interface MainOptions {
  /** The local SQLite file that holds the engine and journal. */
  readonly filename: string
  /** The local parent under which the provider provisions its scratch machine. */
  readonly root: string
}

export interface MainResult {
  /** The child's validated output and the files it wrote. */
  readonly result: typeof SandboxedGreeting.successSchema.Type
  /** How many machines the provider was asked for during this `main`. */
  readonly acquisitions: number
}

/** Runs the parent once over `filename` and closes every acquired layer before resolving. */
export const main = (options: MainOptions): Promise<MainResult> =>
  Effect.runPromise(
    Effect.gen(function*() {
      const fs = yield* FileSystem.FileSystem
      const spawner = yield* ChildProcessSpawner
      const directory = DirectorySandbox.make({ fs, spawner, root: options.root })
      let acquisitions = 0
      const provider: Sandbox.Provider = {
        acquire: (session) => {
          acquisitions++
          return directory.acquire(session)
        }
      }
      const stack = Layer.mergeAll(
        SandboxedFlow.toLayer(RunGreet, Greet, ({ executionId }) => ({
          provider,
          session: `greet:${executionId}`,
          entry: new URL("./sandboxed-child.ts", import.meta.url),
          collectDiff: true
        })),
        Interpreter.layer(SandboxedGreeting)
      ).pipe(
        Layer.provideMerge(Action.layerImplementations),
        Layer.provideMerge(durableEngine(options.filename, "examples-sandboxed-flow"))
      )

      const result = yield* SandboxedGreeting.execute(
        { name: "Ada" },
        { executionId: "sandboxed-greeting" }
      ).pipe(
        Effect.provide(stack),
        Effect.scoped
      )
      return { result, acquisitions }
    }).pipe(
      Effect.provide(
        NodeHost.layerContained().pipe(
          Layer.provide(ProcessLedger.layerMemory({ hostId: "examples-sandboxed-flow", ownerPid: process.pid }))
        )
      ),
      Effect.orDie
    )
  )
```
