# Smithers — full documentation

> Durable AI workflow orchestration as a JSX runtime.
> Repo: github.com/smithersai/smithers · Package: smithers-orchestrator (npm)

This is the complete agent-facing Smithers documentation in one file. It is the concatenation of every fragment listed in /llms.txt.

Audience split: humans should read the For Humans Guide on the docs site and talk to their coding agent. Agents should read this file, operate Smithers for the human, verify the run, and report evidence back.

The everyday agent surface (runtime, JSX, CLI, components, recipes, types, errors) is the first section below; the opt-in topics follow. Only /llms.txt and /llms-full.txt are served on the docs site, so read this file rather than fetching per-topic fragment URLs.

Sections included in this file:
  1. Core: runtime, JSX, CLI, components, recipes, types
  2. Memory: cross-run memory
  3. OpenAPI tools: tool generation from a spec
  4. Observability: HTTP server, gateway, MCP, OpenTelemetry
  5. Effect: low-level Effect-ts integration
  6. Integrations: agent runtimes, IDE, CI, ecosystem
  7. Events: full SmithersEvent discriminated union

Changelogs are not included; see /docs/changelogs/ on the docs site.

===============================================================================
# Smithers

> Smithers — durable AI workflow orchestration as a JSX runtime.
> Repo: github.com/smithersai/smithers · Package: smithers-orchestrator (npm)

This file is the agent-facing core Smithers documentation. It is for Claude, Codex, and other AI harnesses operating Smithers for a human. Read top to bottom for the runtime, agent operating playbook, JSX surface, CLI, and components.

Human-facing docs live on the website under the For Humans Guide. Humans ask their agent for outcomes; agents consume these llms files and operate Smithers.

Opt-in topics cover features most users do not need. They are also sections of the full bundle at /llms-full.txt (only /llms.txt and /llms-full.txt are served on the docs site):
  - Memory (cross-run state)
  - OpenAPI tools
  - Observability + HTTP server
  - Effect-ts authoring API
  - Integrations + CLI agents
  - Event types (full union)

Changelogs are not included; see /docs/changelogs/ on the docs site.

---

## Introduction

> What Smithers is and when to use it.

Smithers orchestrates AI coding agents at scale with composable, model- and harness-agnostic workflows. Most workflow systems fail quietly. A crash mid-run means lost work and a manual restart from scratch. Smithers solves this with a render-loop execution model: every completed step is persisted immediately, so the runtime always knows exactly what has finished and what to run next.

<Note>
This page is in the **For Agents** track. It is for AI harnesses and workflow authors who
need the runtime model. Human-facing docs live in the **For Humans** Guide, starting at
What Smithers Is.
</Note>

Smithers runs your workflow on a render loop: each frame it asks "what has finished, and what can start now?" Tasks produce outputs validated by Zod schemas; the runtime persists them to SQLite. Crashes, restarts, and approvals are first-class, and the runtime resumes from the last persisted state without re-running completed work.

**Default authoring path - MDX prompts.** For most workflows you edit a plain Markdown file in `.smithers/prompts/` and the seeded TypeScript skeleton picks it up automatically. No build step, no TypeScript required. See MDX Workflow Authoring.

**Advanced authoring - TypeScript SDK.** When you need branching, schemas, parallel fan-out, or loops, write the workflow as a JSX tree. Smithers renders the tree and drives execution:

```tsx
<Workflow name="review">
  <Sequence>
    <Task id="analyze" output={outputs.analysis} agent={analyst}>
      {`Review ${ctx.input.repo}`}
    </Task>
    {analysis ? (
      <Task id="fix" output={outputs.fix} agent={fixer}>
        {`Fix these issues:\n${analysis.issues.map(i => `- [${i.severity}] ${i.file}:${i.line} - ${i.description}`).join("\n")}`}
      </Task>
    ) : null}
  </Sequence>
</Workflow>
```

Use Smithers when:

- order matters across multiple AI or compute steps
- you need crash recovery
- humans must approve or answer questions mid-run
- different tasks need different models, tools, or policies
- operators need the Gateway API to launch, stream, and approve runs programmatically

Don't use it for a single prompt → single response. Use your model provider's SDK directly; Smithers adds no value there.

## Read next

- MDX Workflow Authoring to change what your agent does without writing TypeScript.
- Tour for a working code-review example (TypeScript SDK).
- How It Works for the execution model.
- Why React? for the rationale behind the JSX runtime.

---

## Installation

> Install smithers-orchestrator with the workflow pack, or manually for standalone JSX workflow projects.

Most teams should start with the workflow pack. It gives you a working `.smithers/` directory with seeded workflows, prompts, and agent configuration instead of assembling the project structure by hand.

<Note>
  Installation is the one step a human may run by hand. Everything after it
  (starting runs, inspecting them, clearing approvals) is your coding agent's
  job: install the agent skill, then ask the agent
  for outcomes instead of typing Smithers commands yourself.
</Note>

## Always Run with `bunx`

<Warning>
  Agents, MCP configs, and docs should use `bunx smithers-orchestrator <command>`.
  Do **not** use `bunx smithers`: `smithers` is only the installed binary alias. On
  npm, `smithers` is an unrelated package, so `bunx smithers` can download and run
  something else entirely.
</Warning>

- The published npm package is [`smithers-orchestrator`](https://www.npmjs.com/package/smithers-orchestrator).
- `bunx smithers-orchestrator ...` works from any directory and uses the project-pinned dependency when one exists.
- The package also installs a `smithers` binary alias. Use `smithers ...` only when the current environment intentionally provides that binary, such as a project script that resolves `node_modules/.bin/smithers`.
- Avoid global installs: a global `smithers` on PATH can drift from the project version and shadow the project-pinned binary.

If you previously ran `npm i -g smithers-orchestrator`, uninstall it (`npm rm -g smithers-orchestrator`) and switch to `bunx`.

## Updating Smithers

How you update depends on how you run Smithers:

- **`bunx` / `npx` (recommended):** nothing to update. `bunx smithers-orchestrator@latest <command>` always resolves the newest published version, and inside a workflow project it uses the version pinned in `package.json`. To move a pinned project forward, bump the dependency. Running `bunx smithers-orchestrator@latest init` re-scaffolds with the latest, or you can edit the `smithers-orchestrator` version in `.smithers/package.json` directly.
- **Global install:** upgrade with your package manager.

  ```bash
  bun add -g smithers-orchestrator@latest    # bun
  npm install -g smithers-orchestrator@latest # npm
  pnpm add -g smithers-orchestrator@latest    # pnpm
  yarn global add smithers-orchestrator@latest # yarn
  ```

Or let Smithers do it for you. `smithers update` detects how the binary was installed and runs the right command (or, for a `bunx`/project install, prints exactly what to run):

```bash
smithers update            # detect the install method and upgrade
smithers update --check    # just report current vs latest, never change anything
smithers update --dry-run  # print the upgrade command without running it
```

Verify the installed version any time with:

```bash
smithers --version
```

Smithers also checks npm at most once a day and prints a one-line notice on an interactive run when a newer release is available. The same daily check compares your install's SOTA model registry against the published one: when a release ships with new best-in-class models, the notice tells you to run `smithers update` and then `bunx smithers-orchestrator init` so installed workflows move to the latest agents. Disable the passive check with `SMITHERS_NO_UPDATE_CHECK=1` (it is already skipped in CI, in JSON/agent output, and for non-interactive shells).

### Clean reinstall

If you suspect a stale cache or a leftover global symlink shadowing the project version, remove the global install and the runner cache, then reinstall:

```bash
npm rm -g smithers-orchestrator   # or: bun remove -g smithers-orchestrator
which smithers                    # confirm no stale binary remains on PATH
bunx smithers-orchestrator@latest --version
```

`bunx`/`npx` keep their own download caches; appending `@latest` forces them to fetch the newest version rather than reuse a cached one.

## Recommended: Install the Workflow Pack

```bash
bunx smithers-orchestrator init
```

That scaffolds `.smithers/` with files such as:

| Directory / File | Contents |
|---|---|
| `.smithers/workflows/` | Pre-built workflows (`implement`, `review`, `plan`, `ralph`, `debug`, ...) |
| `.smithers/prompts/` | Shared MDX prompt templates |
| `.smithers/components/` | Reusable TSX components (`Review`, `ValidationLoop`, ...) |
| `.smithers/package.json` | Local workflow project manifest with `smithers-orchestrator` dependency |
| `.smithers/tsconfig.json` | TypeScript config for JSX workflow authoring |
| `.smithers/bunfig.toml` | Bun preload config for MDX workflow prompts |
| `.smithers/preload.ts` | Registers the MDX preload plugin |
| `.smithers/agents/` | User-owned agent config (`claude-code.ts`, `codex.ts`, `opencode.ts`, `antigravity.ts`, `index.ts`), edit to pin models/cwd/systemPrompt; preserved across re-inits |
| `.smithers/agents.ts` | Auto-detected agent configuration (regenerated on each `init`) |
| `.smithers/smithers.config.ts` | Repo-level config (lint, test, coverage commands) |
| `.smithers/tickets/` | Ticket workspace used by ticket-oriented workflows |
| `.smithers/executions/` | Execution artifacts directory preserved across re-inits |
| `.smithers/.gitignore` | Ignore rules for generated workflow state |

To overwrite an existing scaffold:

```bash
bunx smithers-orchestrator init --force
```

## Install the Agent Skill

Smithers is driven by an AI agent (Claude Code, Codex, and friends) not a GUI you
click. Your agent runs Smithers on your behalf: scaffolding workflows, starting
runs, watching them, and clearing approvals. The `smithers` skill makes your agent
fluent in that without making it read the whole docs site first, so you reach the
aha moment faster.

`init` auto-installs the curated onboarding skill into detected agents whose
skill directory Smithers can write directly today: Claude Code and Pi (no
`mkdir`, no `curl`). For other agents, use the MCP server plus standing
instructions, or point the agent at `docs-full` below.

To sync the generated Smithers CLI command skill set at any time:

```bash
bunx smithers-orchestrator skills add
```

That writes generated command-level skill files to supported skill locations,
including the canonical `~/.agents/skills` directory. It supports `--no-global`
for project-scoped installs; unlike `mcp add`, it has no supported `--agent`
target filter.

The curated onboarding skill installed by `init` ships the full docs bundle
(`llms-full.txt`) next to its `SKILL.md`, so supported agents can read the exact
API on demand. Once Smithers is wired into your agent, just ask for the outcome,
*"orchestrate an agent to add rate limiting and keep iterating until the tests
pass"*, and the agent reaches for Smithers itself.

For agents without a skills directory, point them at
`bunx smithers-orchestrator docs-full` (prints the same bundle) or
`bunx smithers-orchestrator ask "<question>"`.

To install skills where supported **and** register the MCP server into coding
agents on your machine, see Agent Support. It covers Claude
Code, Codex, Cursor, Copilot, Pi, Hermes, OpenClaw, and more.

## When to Use Manual Installation

Use manual installation when embedding Smithers into an existing TypeScript codebase to author a standalone workflow project from scratch.

See JSX Installation for the package list, TypeScript configuration, and optional MDX prompt setup.

## Requirements

- [Bun](https://bun.sh) >= 1.3
- TypeScript >= 5
- Model or provider credentials (e.g. [Anthropic](https://docs.anthropic.com) `ANTHROPIC_API_KEY`)
- A version control system for snapshotting and isolating agent work: [jj (Jujutsu)](https://github.com/jj-vcs/jj) or [git](https://git-scm.com). jj is preferred and powers durability, time-travel, and per-task worktrees.

### Version control

Smithers bundles jj. The optional `@smithers-orchestrator/jj-<platform>` package installs a vendored jj binary for your platform, so a fresh install works with no system jj. Resolution order is:

1. `SMITHERS_JJ_PATH`: point this at a jj binary to override everything.
2. The bundled binary for your platform.
3. `jj` on your `PATH`.

If no bundled binary installed (an unsupported platform, or `--no-optional`) and neither `jj` nor `git` is on `PATH`, runs that need a worktree fail with a message telling you to install one. Check what Smithers found with `bunx smithers-orchestrator workflow doctor` (the `vcs` section reports the resolved jj and git).

If the bundled jj exists but fails with `EACCES`, its executable bit was stripped during packaging or install. Fix the local install with `chmod +x node_modules/@smithers-orchestrator/jj-*/bin/jj`, reinstall, or set `SMITHERS_JJ_PATH` to a known-good `jj` binary.

## After Installation

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Run a seeded workflow immediately.
  </Card>
  <Card title="Set up in your harness" icon="plug" href="/agents/setup">
    Wire Smithers into your agent and grab a copy-paste setup prompt.
  </Card>
  <Card title="Install the agent skill" icon="robot" href="#install-the-agent-skill">
    Make your coding agent fluent in Smithers.
  </Card>
  <Card title="CLI Quickstart" icon="terminal" href="/cli/quickstart">
    The operational command cheatsheet.
  </Card>
  <Card title="JSX installation" icon="code" href="/jsx/overview">
    Manual TSX authoring setup.
  </Card>
  <Card title="Project structure" icon="folder-tree" href="/guides/project-structure">
    How a standalone workflow project fits together.
  </Card>
  <Card title="Tools integration" icon="screwdriver-wrench" href="/integrations/tools">
    The built-in tool sandbox.
  </Card>
</CardGroup>

---

## Quickstart

> Scaffold and run a Smithers workflow in two commands.

<Note>
  The commands below are operated by the coding agent. If you are an agent, run
  them yourself on the human's behalf; never hand them to the human. If you are
  a human, ask your agent for the outcome and it runs these for you; the initial
  `init` is the only command worth typing by hand before an agent is set up.
</Note>

<Steps>
  <Step title="Scaffold a project">
    `init` creates `.smithers/` (workflows, prompts, components, agent config). Add
    `--template <id>` when you want the result to include a starter command and
    follow-up notes; run `bunx smithers-orchestrator starters` to browse template IDs.

    ```bash
    bunx smithers-orchestrator init
    bunx smithers-orchestrator init --template idea-to-tickets
    ```
  </Step>
  <Step title="Run a workflow">
    Point a seeded workflow at a prompt and Smithers starts a durable run.

    ```bash
    bunx smithers-orchestrator workflow run implement --prompt "Add rate limiting"
    ```
  </Step>
  <Step title="Inspect the run">
    Watch run state, tail the event log, and read structured output as it executes.

    ```bash
    bunx smithers-orchestrator ps
    bunx smithers-orchestrator inspect RUN_ID
    bunx smithers-orchestrator logs RUN_ID --tail 20
    ```
  </Step>
  <Step title="Resume after a crash">
    Every completed step is persisted, so a crashed or stopped run resumes from its
    last checkpoint.

    ```bash
    bunx smithers-orchestrator workflow run hello --run-id RUN_ID --resume true
    ```
  </Step>
</Steps>

<Tip>
  `init` installed the smithers skill for your coding agents automatically. Just ask for the
  outcome (*"orchestrate an agent to add rate limiting and keep iterating until the tests
  pass"*) and your agent runs Smithers for you.
</Tip>

For a full worked example, see the Tour. For every CLI command see the CLI catalog.

---

## Starters

> Choose a plain-English outcome and the command your agent runs for it.

Smithers ships with starter workflows for when you want a result before your agent writes any workflow code. The commands on this page are what the coding agent runs: a human picks the outcome and asks their agent (or names the starter); the agent runs the matching command. Reading this as an agent? Run them yourself; never hand them to the human. Browse the starter gallery from any repo:

```bash
bunx smithers-orchestrator starters
```

First-time setup:

```bash
bunx smithers-orchestrator init --add-agents
```

Or initialize with guided next steps for one template:

```bash
bunx smithers-orchestrator init --template idea-to-tickets
```

Template IDs:

Pass any ID from this table to `init --template <id>` to scaffold that starter.

| Starter | Best for | Workflow | Command |
| --- | --- | --- | --- |
| `idea-to-tickets` | founders, product, operations, engineering | `tickets-create` | `bunx smithers-orchestrator init --template idea-to-tickets` |
| `launch-checklist` | launch owners and operators | `plan` | `bunx smithers-orchestrator init --template launch-checklist` |
| `customer-incident` | support escalations | `debug` | `bunx smithers-orchestrator init --template customer-incident` |
| `nontechnical-research` | before-build decisions | `research` | `bunx smithers-orchestrator init --template nontechnical-research` |
| `requirements-interview` | vague stakeholder requests | `grill-me` | `bunx smithers-orchestrator init --template requirements-interview` |
| `quality-audit` | release readiness | `audit` | `bunx smithers-orchestrator init --template quality-audit` |
| `test-coverage` | regression prevention | `improve-test-coverage` | `bunx smithers-orchestrator init --template test-coverage` |
| `ship-a-change` | focused product improvements | `research-plan-implement` | `bunx smithers-orchestrator init --template ship-a-change` |
| `mission-mode` | larger approved milestones | `mission` | `bunx smithers-orchestrator init --template mission-mode` |

Each detailed starter prints:

- What outcome to expect
- What context to gather before running
- The exact `bunx smithers-orchestrator workflow run ...` command
- Useful follow-up commands
- When not to use that starter

Filter by audience or goal:

```bash
bunx smithers-orchestrator starters --audience product
bunx smithers-orchestrator starters --goal quality
bunx smithers-orchestrator starters --workflow debug
```

Use JSON when another tool needs the catalog:

```bash
bunx smithers-orchestrator starters --format json
```

---

## Tour

> Build a code-review workflow in six steps. Every core feature shows up.

A code-review workflow built one capability at a time. Each step is a diff against the previous. Reading time: 15 minutes.

## 1. Install and scaffold

```bash
bunx smithers-orchestrator init
bun add smithers-orchestrator zod@^4
bun add -d typescript @types/bun
codex login
```

`init` creates `.smithers/` with seeded workflows, prompts, and components. The
bun deps add Smithers and Zod (schemas); `codex login` makes your Codex
subscription available to workflow workers.

<Note>
**Zod v4 is required.** Smithers introspects your output schemas via Zod v4
internals, so pin `zod@^4`. A Zod **v3** schema fails when building an agent
command with the cryptic error `undefined is not an object (evaluating
'schema._zod.def')`.
</Note>

A minimal `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "smithers-orchestrator",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
```

`jsxImportSource` is the only line specific to Smithers; it routes JSX through the workflow runtime instead of React DOM.

## 2. One-task workflow

```tsx
/** @jsxImportSource smithers-orchestrator */
import { createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  input: z.object({ name: z.string() }), // types ctx.input
  greeting: z.object({ message: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="hello">
    <Sequence>
      <Task id="greet" output={outputs.greeting}>
        {{ message: `Hello, ${ctx.input.name}` }}
      </Task>
    </Sequence>
  </Workflow>
));
```

`createSmithers` registers Zod schemas; each becomes a durable output relation managed by the runtime. `outputs.greeting` is the typed reference for the `greeting` schema; using it as the `output` prop gives compile-time checks (typo `outputs.greting` is a type error).

The `input` key is special: its schema types `ctx.input` (so `ctx.input.name` is a checked `string`, not `unknown`). Every other key is an output table. Omit `input` and `ctx.input` is untyped, forcing a defensive guard on each field (`ctx.input?.name ?? "world"`). Input fields also arrive as supplied, with no Zod defaults applied, so coalesce any field you do not require. The other schemas (`greeting` here) are the workflow's outputs.

This Task has no `agent`, just a literal value. Run it.

```bash
bunx smithers-orchestrator up workflow.tsx --input '{"name":"world"}'
```

Inspect:

```bash
bunx smithers-orchestrator ps                  # find the run id
bunx smithers-orchestrator inspect RUN_ID    # structured state
bunx smithers-orchestrator output RUN_ID greet --pretty  # typed node output
```

For a controller or custom monitor, read the same output through Gateway
`getNodeOutput({ runId, nodeId: "greet" })`. Do not query the backing store;
SQLite/PGlite/Postgres are interchangeable runtime details.

## 3. Add an agent task

Replace the literal Task with an agent Task whose output is structured.

```tsx
import { CodexAgent, createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  input: z.object({ repo: z.string() }),
  analysis: z.object({
    summary: z.string(),
    issues: z.array(z.object({
      file: z.string(),
      line: z.number(),
      severity: z.enum(["low", "medium", "high"]),
      description: z.string(),
    })),
  }),
});

const analyst = new CodexAgent({
  model: "gpt-5.6-sol",
  instructions: "You are a senior code reviewer. Return structured JSON.",
});

export default smithers((ctx) => (
  <Workflow name="review">
    <Sequence>
      <Task id="analyze" output={outputs.analysis} agent={analyst}>
        {`Review the code in ${ctx.input.repo} and return analysis as JSON.`}
      </Task>
    </Sequence>
  </Workflow>
));
```

The runtime injects a JSON-schema description of `outputs.analysis` into the prompt, parses the agent's response, validates against Zod, and persists. Validation failure triggers a retry.

## 4. A second task that depends on the first

Tasks see each other's outputs through `ctx.outputMaybe(...)`. An incomplete upstream returns `undefined`; on the next render frame the upstream output appears and the downstream Task mounts.

For the common case of one Task consuming exactly one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` children callback is the more ergonomic form; reach for `ctx.outputMaybe` when the guard needs to inspect content or gate more than one sibling.

```tsx
import { CodexAgent, createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const AnalysisSchema = z.object({
  summary: z.string(),
  issues: z.array(z.object({
    file: z.string(),
    line: z.number(),
    severity: z.enum(["low", "medium", "high"]),
    description: z.string(),
  })),
});

const { Workflow, smithers, outputs } = createSmithers({
  input: z.object({ repo: z.string() }),
  analysis: AnalysisSchema,
  fix: z.object({
    patch: z.string(),
    filesChanged: z.array(z.string()),
  }),
});

const analyst = new CodexAgent({
  model: "gpt-5.6-sol",
  instructions: "You are a senior code reviewer. Return structured JSON.",
});

const fixer = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  instructions: "Write minimal, correct fixes as a unified diff.",
});

export default smithers((ctx) => {
  const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });

  return (
    <Workflow name="review">
      <Sequence>
        <Task id="analyze" output={outputs.analysis} agent={analyst}>
          {`Review ${ctx.input.repo}`}
        </Task>
        {analysis ? (
          <Task id="fix" output={outputs.fix} agent={fixer}>
            {`Fix these issues:\n${analysis.issues.map(i =>
              `- [${i.severity}] ${i.file}:${i.line} - ${i.description}`
            ).join("\n")}`}
          </Task>
        ) : null}
      </Sequence>
    </Workflow>
  );
});
```

Render 1: only `analyze` is mounted. Render 2 (after `analyze` finishes): `analysis` is populated, `fix` mounts and runs. That is the entire reactivity story: no hooks, no subscriptions, JSX conditionals over persisted state.

Same shape works for branching, parallel groups, and loops. A `?:` conditional
is the inline form; `<Branch>` is the declarative form when
you want explicit `then`/`else` (it takes those as props, not children):

```tsx
<Parallel maxConcurrency={3}>
  <Task id="lint"  output={outputs.lint}  agent={linter}>...</Task>
  <Task id="test"  output={outputs.test}  agent={tester}>...</Task>
  <Task id="audit" output={outputs.audit} agent={auditor}>...</Task>
</Parallel>

<Branch
  if={!!analysis && analysis.issues.length > 0}
  then={<Task id="fix"  output={outputs.fix}  agent={fixer}>...</Task>}
  else={<Task id="ship" output={outputs.ship} agent={shipper}>...</Task>}
/>

<Loop until={!!review?.approved} maxIterations={5}>
  <Task id="implement" output={outputs.impl} agent={implementer}>...</Task>
  <Task id="review"    output={outputs.review} agent={reviewer}>...</Task>
</Loop>
```

## 5. An approval gate

Pause for a human. The runtime persists the pending decision and exits cleanly; the operating agent relays the question to the human, then approves or denies through the CLI; resume picks up from the gate.

```tsx
import { Approval } from "smithers-orchestrator";

{analysis ? (
  <Approval
    id="confirm-fix"
    output={outputs.confirmFix}
    request={{
      title: `Apply fixes for ${analysis.issues.length} issues?`,
      summary: analysis.summary,
    }}
    onDeny="skip"
  >
    {/* children rendered after approval */}
  </Approval>
) : null}

{ctx.outputMaybe(outputs.confirmFix, { nodeId: "confirm-fix" })?.approved ? (
  <Task id="fix" output={outputs.fix} agent={fixer}>
    {`Apply patches`}
  </Task>
) : null}
```

Operator side (you, the agent, run these on the human's behalf; never hand them to the human):

```bash
bunx smithers-orchestrator ps --status waiting-approval     # find paused runs
bunx smithers-orchestrator inspect RUN_ID                  # see the request
bunx smithers-orchestrator approve RUN_ID --node confirm-fix --by alice
bunx smithers-orchestrator up workflow.tsx --run-id RUN_ID --resume true
```

`onDeny` controls behavior on rejection: `"fail"` aborts the run, `"continue"` proceeds without the approved branch, `"skip"` skips the gated tasks.

## 6. Crash, then resume

Every completed task's output sits in SQLite. A crash, kill, or restart loses no work; the next run with `--resume true` skips finished tasks.

```bash
bunx smithers-orchestrator up workflow.tsx --input '{"repo":"."}' --run-id review-1
# ...analyze finishes, fix is mid-flight, you Ctrl+C

bunx smithers-orchestrator up workflow.tsx --run-id review-1 --resume true
# analyze is skipped (already in DB), fix re-runs from scratch (was incomplete)
```

<Frame caption="The same crash-and-resume mechanic: a run is killed mid-task, then resuming skips the finished work and re-runs only the interrupted task from its last persisted frame.">
  <img src="/images/why/crash-resume.gif" alt="A Smithers run is killed partway through, then resumes: the completed task is skipped, the in-flight task re-runs as a new attempt, and the run finishes" />
</Frame>

In-flight attempts are marked stale and re-tried; finished tasks are not. Resume is deterministic: same input + same code = same task IDs.

For unattended recovery, run the supervisor:

```bash
bunx smithers-orchestrator supervise --interval 30s --stale-threshold 1m
```

It auto-resumes runs whose owner process died.

## What you skipped (and where to find it)

- **Time travel** (replay a frame, fork a run, diff two runs): `bunx smithers-orchestrator replay|fork|diff|timeline`. See How It Works → Time travel.
- **Scorers** (attach evaluators to Tasks): see Recipes → Scoring tasks.
- **Memory** (cross-run facts and message history): see How It Works → Memory.
- **RAG**, **voice**, **OpenAPI tools**: opt-in fragments. See the index in llms.txt.
- **Tool sandbox** (read/grep/bash with path containment): see Recipes → Tools.

## Read next

- How It Works: the render → execute → persist loop.
- Components: JSX surface reference.
- CLI: every command in one table.
- Recipes: patterns from production workflows.

---

## How It Works

> The render → execute → persist loop, in one page.

Smithers is a React reconciler whose host elements are tasks instead of DOM nodes. Each render produces a snapshot of the workflow plan; the runtime extracts ready tasks from that plan, executes them, persists their outputs, and re-renders. The plan evolves because each render reads the persisted state.

<Note>API reference: Types lists every public type, its fields, and links to source and tests.</Note>

<Frame caption="The whole runtime in one loop: render the tree, extract the ready tasks, execute them, persist their outputs, then re-render against the new state. State is the source of truth; the plan is a pure function of state.">
  <img src="/images/why/render-loop.svg" alt="A four-stage loop: render the workflow tree, extract ready tasks, execute them, persist outputs to SQLite, then re-render against the new state" />
</Frame>

That loop is the entire model. Everything below, including branching, loops, approvals, resume, and time travel, is either a JSX construct that affects rendering or a CLI surface over the persisted state.

## The render loop in detail

1. **Render**. The runtime calls your `smithers((ctx) => ...)` builder. The returned JSX tree is reconciled by React; the reconciler emits a graph of host elements (`smithers:workflow`, `smithers:task`, `smithers:sequence`, `smithers:parallel`, `smithers:branch`, `smithers:loop`, `smithers:approval`, etc.).
2. **Extract**. The runtime walks the tree to produce a `GraphSnapshot`, a flat list of `TaskDescriptor`s. Each descriptor captures: node id, ordinal, dependencies, output schema, agent, retries, timeouts.
3. **Schedule**. The scheduler computes the ready set: tasks whose dependencies have completed, whose enclosing sequence has reached them, whose enclosing branch resolved them, and which fit within `maxConcurrency`.
4. **Execute**. Each ready task runs. Three modes: agent (call the LLM, validate output against the Zod schema, retry on failure), compute (run the function), static (write the literal value).
5. **Persist**. Validated outputs are written to per-schema SQLite tables. Internal `_smithers_*` tables capture node state, attempts, frame snapshots, events, and durable approval/signal state.
6. **Re-render**. The next frame begins with `ctx` reading the updated outputs. Tasks that depended on now-completed outputs mount on this frame and become eligible to run.

The frame is the unit of progress. Time travel, observability, hot reload, and resume all key off the frame number.

## The `ctx` API

`ctx` is the only way the workflow body talks to the runtime.

| Method | Returns | Use for |
|---|---|---|
| `ctx.input` | `T` | The immutable input passed to `runWorkflow`. |
| `ctx.outputs(table)` / `ctx.outputs.<key>` | `Row[]` | All rows for an output schema key or output target. |
| `ctx.outputMaybe(table, { nodeId, iteration? })` | `Row \| undefined` | Conditional rendering; returns `undefined` until the upstream task completes. |
| `ctx.output(table, { nodeId, iteration? })` | `Row` | Same, but throws if missing. Use inside a Task body where the dep is guaranteed. |
| `ctx.latest(table, nodeId)` | `Row \| undefined` | Highest iteration of a node, used inside `<Loop>` to read the previous iteration's output. |
| `ctx.latestArray(value, schema)` | `unknown[]` | Parse a JSON string, scalar, or array and keep entries accepted by `schema.safeParse`. |
| `ctx.iterationCount(table, nodeId)` | `number` | Number of completed iterations for a loop node. |
| `ctx.resolveTableName(table)` / `ctx.resolveRow(table, key)` | `string` / `Row \| undefined` | Low-level helpers for custom table references and exact output lookup. |
| `ctx.runId` / `ctx.iteration` / `ctx.iterations` | `string` / `number` / `Record<string, number> \| undefined` | Identifiers and loop counters for logging and scoped loop reads. |
| `ctx.auth` | `RunAuthContext \| null` | Auth context passed via `RunOptions.auth`. |

Outputs are keyed by `(runId, nodeId, iteration)`. `iteration` is `0` outside loops; inside `<Loop>` each pass writes a new row at the next iteration index.

The `table` argument means the schema key or output target from `createSmithers`
(`"review"` or `outputs.review`), not a raw SQL table name. The runtime resolves
that key to the actual persisted table.

## Tasks: three modes

```tsx
// Agent: call an LLM. Children become the prompt; output validated against schema.
<Task id="analyze" output={outputs.analysis} agent={analyst}>
  {`Review ${ctx.input.repo}`}
</Task>

// Compute: children is a function. Runs at execution time.
<Task id="count" output={outputs.count}>
  {() => fs.readdirSync(ctx.input.dir).length}
</Task>

// Static: children is a plain value. Persisted directly.
<Task id="config" output={outputs.config}>
  {{ region: "us-east-1", retries: 3 }}
</Task>
```

Agent output validation depends on the agent's structured-output capability. If
the agent declares `supportsNativeStructuredOutput = true` (`AnthropicAgent` and
`OpenAIAgent` by default), Smithers passes the Zod schema to
`agent.generate({ outputSchema })`; those wrappers forward it through the AI
SDK native structured-output channel (`Output.object({ schema })`). If the
agent does not declare that flag (most CLI agents, including `ClaudeCodeAgent`),
Smithers injects JSON instructions into the prompt, extracts JSON from the text,
validates it, and warns about the fallback. Validation failure feeds the error
back into a retry attempt, so agents self-correct on schema drift.

Agents can be a fallback chain: `agent={[primary, fallback]}` tries `primary` first and falls through on failure.

## Control flow

Four primitives. Compose freely.

```tsx
<Sequence>            // children execute top-to-bottom; default for <Workflow>
<Parallel maxConcurrency={3}>  // children execute concurrently
<Branch if={cond} then={<A/>} else={<B/>}>
<Loop until={done} maxIterations={5} onMaxReached="return-last">
```

`<Workflow>` implicitly sequences its children. An explicit `<Sequence>` is only needed when nesting sequential groups inside `<Parallel>` or another control-flow primitive.

Use `.map()` and ternaries when the *number* or *presence* of tasks depends on state. Use `<Parallel>` and `<Branch>` for fixed task sets whose execution shape depends on state.

`<Loop>` is the one primitive that re-renders the same body repeatedly: it runs the body, reads the result on the next frame, and re-mounts until `until` holds or `maxIterations` is reached. That cycle is what turns a one-shot agent into one that keeps swinging until the tests are green.

```mermaid
flowchart LR
  R[Render loop body] --> X[Execute tasks]
  X --> Q{until condition met?}
  Q -- no --> R
  Q -- yes --> D[Exit · return last output]
  Q -. maxIterations hit .-> D
  style Q fill:#fef,stroke:#a3a
  style D fill:#dfe,stroke:#3a3
```

## Data flow is unidirectional

Workflow state lives in SQLite. The render function is a pure function of `ctx` (which reads SQLite). Tasks emit outputs; the runtime persists them; the next render reads them. No mutation, no refs, no `useState` for durable values.

This is the same shape as React rendering UI from props/state, except:
- the "DOM" is the task graph
- "events" are task completions
- "state updates" are output writes that the runtime triggers

<Frame caption="State is the only source of truth. Task completions update it, and the next plan is a pure function of that state, so you never mutate the plan by hand.">
  <img src="/images/unidirectional-dataflow.jpg" alt="Unidirectional data flow: action events update state, state maps forward into the execution plan, and the plan registers the next action handlers" />
</Frame>

Three consequences:
- The plan is a **derived value**. Re-render after any state change automatically computes the new plan; you never manually mutate the plan.
- **Time travel works** because every frame is a snapshot of (state → plan).
- **Hot reload works** because reloading the workflow code with the same persisted state produces a new plan; the runtime diffs the two and continues from where you left off.

## Reactivity & React patterns

Smithers JSX is real React. Components, props, children, composition, context, hooks, custom hooks: all work.

```tsx
function useReviewState(ticketId: string) {
  const ctx = useCtx();
  const claudeReview = ctx.latest("review", `${ticketId}:review-claude`);
  return { claudeReview, allApproved: !!claudeReview?.approved };
}
```

`useState` and `useMemo` are process-local; they reset on every render frame. Use them for ephemeral render-time state. **Anything the workflow must remember across crashes goes through `ctx` and a Task output.**

Conditional mounting matters: a Task that doesn't render is not in the plan. No "skipped" placeholder unless you use `<Branch>` or `skipIf`. That's what lets `{analysis ? <Task .../> : null}` work as a clean dependency check. For the common case of one Task consuming one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` children callback is the more ergonomic form of the same check.

## Approvals & human-in-the-loop

Two surfaces.

`needsApproval` on a Task is a **gate**: pause before execution, no decision data:

```tsx
<Task id="deploy" output={outputs.deployResult} agent={deployer} needsApproval>
  Deploy to production.
</Task>
```

`<Approval>` is a **decision node**: it produces a typed `ApprovalDecision` row that downstream rendering can branch on:

```tsx
<Approval
  id="ship-decision"
  output={outputs.shipDecision}
  request={{ title: "Ship release v1.4?", summary }}
  onDeny="continue"
/>

{ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved
  ? <Task id="release" .../>
  : <Task id="rollback" .../>}
```

Three denial policies: `"fail"` (abort the run), `"continue"` (proceed without the gated branch), `"skip"` (skip the gated tasks but continue siblings).

```mermaid
flowchart TD
  A[Approval node] --> Q{your decision}
  Q -- approved --> G[Gated branch runs]
  Q -- denied --> P{onDeny}
  P -- fail --> F[Abort the run]
  P -- continue --> C[Proceed without the gated branch]
  P -- skip --> S[Skip gated tasks · run siblings]
  style F fill:#fde,stroke:#c33
  style C fill:#dfe,stroke:#3a3
  style S fill:#def,stroke:#36c
```

Operator side is identical for both (you, the agent, run these on the human's behalf; never hand them to the human):

```bash
bunx smithers-orchestrator ps --status waiting-approval
bunx smithers-orchestrator approve RUN_ID --node ship-decision --by alice
bunx smithers-orchestrator up workflow.tsx --run-id RUN_ID --resume true
```

`<HumanTask>` is for richer interaction: a human submits arbitrary structured JSON. `<EscalationChain>` and `<ApprovalGate>` are higher-level patterns built from these.

## Durability & resume

The contract: **a completed task is never re-executed.** Resume loads persisted state, validates the environment (workflow source hash + VCS revision must match the original run), cleans stale in-progress attempts (>15 min without a heartbeat are abandoned), re-renders, and continues.

```bash
bunx smithers-orchestrator up workflow.tsx --run-id RUN_ID --resume true
```

<Frame caption="A run is killed mid-flight: the completed task is skipped on resume, the interrupted one re-runs as a fresh attempt, and the run finishes from the last persisted frame instead of starting over.">
  <img src="/images/why/crash-resume.gif" alt="A Smithers run crashes partway through, then resumes: the finished task is skipped, the in-flight task re-runs as a new attempt, and the remaining tasks execute" />
</Frame>

For resume to work, **task IDs must be stable across renders.** Derive them from data, not from indices or timestamps:

```tsx
{tickets.map((t) => <TicketPipeline key={t.id} id={`${t.id}:work`} .../>)}
// NOT id={`work-${i}`} or id={`work-${Date.now()}`}
```

Same rule as React keys. A changed task ID looks like a new task to the runtime, and an old task whose ID disappeared is dropped from the plan.

The supervisor auto-resumes runs whose owner process died:

```bash
bunx smithers-orchestrator supervise --interval 30s --stale-threshold 1m
```

## Session snapshots & fork

Every agent task persists its conversation as a durable session snapshot alongside its output. A later task can start from a **copy** of that context with `fork`:

```tsx
<Task id="plan" agent={claude} output={outputs.plan}>Make a plan.</Task>
<Task id="implement" agent={claude} fork="plan" output={outputs.patch}>Implement the plan.</Task>
```

`fork` is immutable: it copies the source conversation into a fresh, independent session and submits the new prompt. The source is never mutated, so many tasks can fork the same source in parallel, and a forked task can itself be forked. Because the snapshot is read from persisted state on each attempt, fork is resume-safe and the source is never re-executed. Inside a `<Loop>`, `fork` resolves to the latest completed snapshot for that task id. See `<Task>` fork.

## Caching

Per-Task caching with explicit invalidation:

```tsx
<Task
  id="expensive-analysis"
  output={outputs.analysis}
  agent={analyst}
  cache={{
    by: (ctx) => ({ repo: ctx.input.repo, version: "v3" }),
    version: "v3",
  }}
>
  Analyze {ctx.input.repo}
</Task>
```

Cache key = `cache.by(ctx)` + `cache.version` + the schema signature (SHA-256 of the table structure). A schema change invalidates the cache automatically.

Don't cache side-effect tasks (deploys, emails, mutations). Caching is for pure work that's expensive to recompute.

## Time travel

Every frame commit produces a `GraphSnapshot`.

```bash
bunx smithers-orchestrator timeline RUN_ID           # frames + forks
bunx smithers-orchestrator diff RUN_ID NODE_ID     # node DiffBundle
bunx smithers-orchestrator fork workflow.tsx --run-id RUN_ID --frame 5 --reset-node analyze
bunx smithers-orchestrator replay workflow.tsx --run-id RUN_ID --frame 5 --restore-vcs
```

Replay with `--restore-vcs` checks out the jj revision the snapshot was taken at, so re-execution sees the same source code as the original run.

## Scorers (evals)

Attach evaluators to a Task. They run **after** completion and never block.

```tsx
import { schemaAdherenceScorer, latencyScorer } from "smithers-orchestrator/scorers";

<Task
  id="analyze"
  output={outputs.analysis}
  agent={analyst}
  scorers={{
    schema: { scorer: schemaAdherenceScorer() },
    latency: { scorer: latencyScorer({ targetMs: 5000 }) },
  }}
>
  Analyze...
</Task>
```

Five built-ins: `schemaAdherenceScorer`, `latencyScorer`, `relevancyScorer`, `toxicityScorer`, `faithfulnessScorer`. Sampling: `all` / `ratio` / `none`. Custom scorers and LLM-judge scorers with `createScorer` and `llmJudge`.

Five delegation scorers back the delegation-chain workflow: `pocJudgmentScorer` (probe judgment; false negatives punished hardest), `planSolidityScorer` (post-execution replan churn), `estimateAccuracyScorer` (forecast vs. actual cost/time/tokens), `tierFitScorer` (was the intelligence tier right), and `humanPollScorer` (end-of-run user poll), combined by `delegationRunScore` / `weightedScore`, with `extractDelegationEvents` and `resolvePlanningNodes` as the shared event readers.

The public scorer surface also exports `runScorersAsync`, `runScorersBatch`, `aggregateScores`, the `smithersScorers` table, token-cost helpers (`modelTokenPrices`, `estimateCostUsd`), and scorer metrics (`scorersStarted`, `scorersFinished`, `scorersFailed`, `scorerDuration`).

```bash
bunx smithers-orchestrator scores RUN_ID
```

## Memory (cross-run state)

Memory is **state that survives across runs** -- namespaced facts and message history. Not the same as task outputs (which are per-run).

Three layers, four namespaces (`workflow`, `agent`, `user`, `global`). Three processors (`TtlGarbageCollector`, `TokenLimiter`, `Summarizer`). See the full docs bundle for the full surface.

## Tools, execution environment & sandboxing

Five built-in tools (`read`, `write`, `edit`, `grep`, `bash`) sandboxed to `rootDir`. Symlinks, network, and timeouts are denied by default; `--allow-network` opens bash to the network.

Least-privilege per task:

```tsx
import { AnthropicAgent } from "smithers-orchestrator";
const analyst     = new AnthropicAgent({ model, instructions: "..." }); // no tools
const reviewer    = new AnthropicAgent({ model, instructions: "...", tools: { read, grep } });
const implementer = new AnthropicAgent({ model, instructions: "...", tools: { read, write, edit, bash } });
```

`defineTool` builds custom tools. Mark side-effecting ones with `sideEffect: true` and use `ctx.idempotencyKey` so retries don't double-fire.

### Where agents run & what's billed

There are **two execution modes**, decided by the agent class, not by a per-turn setting:

- **SDK agents run in-process.** `AnthropicAgent` and `OpenAIAgent` (and `HermesAgent`) extend the AI SDK's `ToolLoopAgent`, and the opt-in `ElizaAgent` wraps an elizaOS `AgentRuntime` in-process; all make plain HTTPS calls to a provider. **No subprocess, no container** -- the agent's "environment" is your process. These are the only agents that run unchanged inside a JS-only serverless runtime (a Cloudflare Worker, a Vercel function).
- **CLI / full-OS agents run as a child process.** `ClaudeCodeAgent`, `CodexAgent`, `OpenCodeAgent`, and every other CLI agent extend `BaseCliAgent`, which spawns the vendor binary via `node:child_process`. **By default that child process runs on the host, in `rootDir`** -- the same machine driving the run. There is no automatic per-turn container.

<Note>
  **The default is: no container.** SDK agents are API calls in your process; CLI agents are local child processes in `rootDir`. Neither spins up an isolated OS per turn -- a full OS environment is **opt-in** via `<Sandbox>`. Model tokens/subscription, host compute (when local), and sandbox provider compute (when you opt in) are the three cost axes. The full model -- including which harnesses are serverless, warm vs cold containers, and the three meanings of "sandbox" -- is on Where agents run, sandboxes & cost.
</Note>

Use `AnthropicAgent`/`OpenAIAgent` when you want API-billed SDK agents with
native structured output. Use `ClaudeCodeAgent`, `CodexAgent`, and the other CLI
agents when you want the vendor CLI/subscription surface; they can still produce
typed task outputs, but the engine uses the prompt-and-parse fallback unless the
agent documents a native structured-output opt-in.

## Common gotchas

- **Stable task IDs.** `id="implement-${i}"` or `id={Math.random()}` breaks resume. Derive from data.
- **`useState` is not durable.** Resets on every render. Persist via `ctx` and a Task.
- **Input is immutable.** Resuming with different `--input` is an error; the input is persisted at first run.
- **Adding a schema field auto-migrates (SQLite).** Adding a field to a typed input or output Zod schema does not require recreating `smithers.db`. On every boot the runtime runs `CREATE TABLE IF NOT EXISTS` and then `ALTER TABLE ... ADD COLUMN` for any column the schema introduced, so new fields are added in place. If you still see `SQLiteError: table input has no column named X`, you are on a build before this boot-time migration landed (or a Postgres-backed DB, which does not auto-add columns yet); upgrade with `bunx smithers-orchestrator --version` or start a fresh run. Renaming or removing a field, or changing a field's type, is not migrated and does need a fresh DB.
- **Code changes block resume.** A workflow source change = a different workflow. Hot reload applies changes within a running frame; resume validates the source hash of the original run. If you stop a run and the source has changed, resume is blocked. Start a new run, don't resume across edits.
- **Cached output is re-validated.** Schema drift after caching is caught (the validator rejects the stale row), so the cache misses safely.
- **Side-effect tasks should not be cached.** Pure work only.

The full list, with the fix for each, is in Common Footguns.

## Read next

- Components: JSX surface reference.
- CLI: every command.
- Recipes: patterns from production workflows.
- Types: public TypeScript surface.

---

## Where agents run, sandboxes & cost

> The execution model behind Smithers agents -- in-process SDK agents vs full-OS CLI agents, when a container spins up, how sandboxes are billed, and which harnesses are serverless.

If you have read How it Works and still cannot tell *where an
agent actually runs*, what it costs, or which harnesses are "serverless", this
page is the answer. It is the one place that states the execution model plainly.

## The default is: no container

Which execution mode you get is decided by the **agent class**, not by a per-turn
setting:

- **SDK agents run in-process.** `AnthropicAgent` and `OpenAIAgent` (and
  `HermesAgent`) extend the AI SDK's `ToolLoopAgent`; the opt-in `ElizaAgent`
  wraps an elizaOS `AgentRuntime` in the same process. All of them make plain
  HTTPS calls to a model provider. There is **no subprocess and no
  container** -- the agent's "environment" is your own process. These are the only
  agents that run unchanged inside a JS-only serverless runtime (a Cloudflare
  Worker, a Vercel function).
- **CLI / full-OS agents run as a child process.** `ClaudeCodeAgent`,
  `CodexAgent`, `AntigravityAgent`, `OpenCodeAgent`, and every other CLI agent extend
  `BaseCliAgent`, which spawns the vendor binary (`claude`, `codex`, `opencode`,
  …) via `node:child_process`. **By default that child process runs on the host,
  in `rootDir`** (`this.cwd ?? options?.rootDir ?? process.cwd()`) -- the same machine
  that is driving the run. There is no automatic per-turn container.

<Note>
  **Neither mode spins up an isolated OS per turn.** SDK agents are API calls in
  your process; CLI agents are local child processes in `rootDir`. A full OS
  environment is **opt-in** -- you wrap the work in `<Sandbox>`.
</Note>

### Three common guesses -- all reasonable, all wrong by default

Newcomers consistently guess the same three things. Here is the correction:

| Guess | Reality |
|---|---|
| "Each agent turn spins up a fresh sandbox that pauses between turns." | No -- by default no agent is containerized at all. Containerization is an explicit `<Sandbox>` choice, and pause/resume is a *provider* feature you opt into. |
| "OpenCode is serverless, Claude Code never is." | Half right. OpenCode is **not** in-process serverless -- it is still a spawned binary that needs a container; it just goes serverless *via a cheap cold per-boundary container* when its API keys come from the environment. Claude Code is "never" **only** in subscription mode; with an API key it is exactly as serverless as OpenCode. |
| "The real axis is which vendor." | The real axis is **in-process SDK vs subprocess CLI**, and within CLI, **stateless (API key) vs stateful (subscription OAuth + resumable session on disk)**. |

## What you pay for -- three billing axes

There is no single "cost of running an agent." There are three independent axes,
and a given run may touch one, two, or all three:

| Axis | What it is |
|---|---|
| **Model tokens / subscription** | The model provider bills per input/output token (SDK agents, or CLI agents in API-key mode), or draws down a subscription quota (Claude Pro/Max, ChatGPT Plus/Pro) in subscription mode. |
| **Host compute** | When a CLI agent runs locally (the default), it consumes the machine that is driving the run -- your laptop, a CI runner, a long-lived server. |
| **Sandbox / provider compute** | Only when you opt into `<Sandbox>`: the provider's container or VM wall-clock, from create → run → teardown (longer if kept warm or left idling), plus any provider storage. |

## Opting into a full OS: `<Sandbox>`

`<Sandbox>` runs a child workflow (or a single step)
inside an isolated runtime -- whole-graph, per-step, or mixed, your choice. It
renders as exactly **one scheduler task per boundary** (not per agent turn);
children never become parent-run tasks. The runtime is pluggable
(`bubblewrap`, `docker`, `codeplane`, `cloudflare`) or any custom provider you
register -- e.g. the Freestyle VM example adapter.

The lifecycle at a `<Sandbox>` boundary is: **create the sandbox when the task
starts → run the harness → capture a diff bundle of what changed → tear it down**
(unless you keep it). Cleanup, idle timeout, and reuse are the **provider's**
decision, not a Smithers-core per-turn guarantee. The Cloudflare provider, for
example, creates one container keyed `${runId}-${sandboxId}`, defaults
`cleanup: "destroy"`, and can `keep` the container or hold it warm with
`keepAlive`. A Freestyle VM exposes explicit `start()` / `stop()` / `fork()` /
`delete()` and `idleTimeoutSeconds`.

<Note>
  Running a CLI agent in a fresh per-turn container is a **composition you
  author**, not a built-in switch: wrap the agent's `<Task>` in a child workflow
  behind `<Sandbox provider={…}>`. No agent adapter creates or pauses a container
  on its own -- `BaseCliAgent` always spawns on the local host.
</Note>

## Statefulness -- warm vs cold containers

CLI agents keep credentials and resumable sessions **on disk**, which decides
whether a cold per-turn container is viable:

- **API-key billing → cold containers are fine.** Pass `apiKey` and the agent
  forwards `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`; sessions are stateless per
  turn, so a fresh (cold) container each turn works.
- **Subscription billing → needs a warm/persistent environment.**
  `ClaudeCodeAgent` *clears* `ANTHROPIC_API_KEY` so the CLI bills your Claude
  Pro/Max subscription, and it reads credentials materialized by a one-time
  interactive `/login` at `<CLAUDE_CONFIG_DIR>/.credentials.json`. `CodexAgent`
  mirrors this with `CODEX_HOME` / `auth.json` and bills the ChatGPT
  subscription. A cold container has none of that on disk, so subscription mode
  needs the credential/session directory to persist -- a sticky/warm container or
  a mounted credential volume.

## Serverless compatibility at a glance

| Harness | Mode | Serverless verdict | Cost driver |
|---|---|---|---|
| `AnthropicAgent`, `OpenAIAgent`, `HermesAgent`, `ElizaAgent` | In-process SDK | ✅ Agent is fully serverless -- runs inside a Worker/function, no container | Model tokens |
| `OpenCodeAgent` | CLI child process | ⚠️ Serverless via a **cold** per-boundary container (`<Sandbox>`) when keys come from env; a subscription `opencode auth login` puts credentials on disk like the others | Tokens + container wall-clock |
| `ClaudeCodeAgent` -- API-key mode (`apiKey` set) | CLI child process | ⚠️ Same as OpenCode: cold per-boundary container works | Tokens + container wall-clock |
| `ClaudeCodeAgent` / `CodexAgent` -- subscription mode (default) | CLI child process | ⚠️ Needs a **warm/sticky** container or mounted credential volume | Subscription quota + warm container time |
| `AntigravityAgent`, `PiAgent`, `KimiAgent`, `ForgeAgent`, `AmpAgent`, others | CLI child process | ⚠️ Container required; warm vs cold depends on the CLI's session/credential handling | Tokens/subscription + container time |

<Warning>
  **Not fully serverless end-to-end today.** The DB layer (dialect + Cloudflare
  Durable-Object-SQLite / D1 descriptors) and the SDK agents are Worker-native,
  but the **core engine that advances every run** uses `node:fs` and
  `node:child_process` (it materializes git/jj worktrees on a real filesystem),
  and the gateway is a `node:http` server. So today the engine runs on a **Bun**
  host with a filesystem (a container on ECS/Cloud Run/GKE/a VM). Two efforts are
  in flight: the **Node runtime** (Vercel Node functions, Lambda) is closest --
  it has `fs` + `child_process`, the engine module now imports cleanly under
  plain Node, and the platform layer is injectable
  (`RunOptions.effectPlatformLayer` accepts `NodeContext.layer`); a run now
  completes end-to-end under plain Node (regression-tested: a compute-task
  workflow on PGlite, with the worker-task dispatch path falling back to
  in-memory message storage instead of bun-sqlite), and agent-task and gateway
  validation under Node remain. **Isolates** (Cloudflare Workers, Vercel Edge) are further out and
  also need the `node:fs`/`node:child_process` seam; a Worker can host the
  **storage + in-process-agent** path today, but not the run driver.
</Warning>

## "Sandbox" means three different things

This overloading is the single biggest source of confusion -- name the sense you
mean:

1. **Tool sandbox** -- the built-in tools (`read`/`write`/`edit`/`grep`/`bash`)
   jailed to `rootDir`, with symlinks/network/timeouts denied by default. A
   path/permission jail, **not** an OS boundary.
2. **A CLI agent's own internal policy** -- e.g. Codex's
   `sandbox: "read-only" | "workspace-write" | "danger-full-access"`
   (seatbelt/seccomp inside the vendor binary). Passed straight through to the
   CLI; unrelated to the other two.
3. **The `<Sandbox>` component** -- compute isolation: a
   provider-backed container/VM that runs a child workflow. This is the only one
   that gives an agent "a real computer."

## Read next

- Sandbox component: the JSX surface and providers.
- CLI agents: per-harness auth, billing, and sessions.
- SDK agents: in-process provider-backed agents.
- Cloudflare: Workers, Durable Object SQLite, and the sandbox provider.

---

## Context Engineering

> The layered control system behind a reliable agent workflow, the three execution levers, and how Smithers does it for you.

Writing a better prompt is the smallest lever. Getting an agent to reliably finish
real work is a **layered control system**, and most of those layers live *around*
the model, not inside the prompt. Smithers owns the outer layers so you can
describe an outcome and let the system assemble the rest.

## The layers

| Layer | What it controls | Where it lives in Smithers |
| --- | --- | --- |
| **Prompt engineering** | instructions, examples, role, output format, success criteria | the prompt `.mdx` a `<Task>` renders |
| **Context engineering** | what information, tools, memory, schemas, and state enter the model each step | the workflow graph + memory + typed outputs |
| **Harness engineering** | runtime, tools, conventions, permissions, retries, fresh-context loops | `agents.ts`, sandboxes, tools, `repoCommands` |
| **Workflow engineering** | order, parallelism, review loops, approvals, resumability, artifacts | the Smithers runtime itself |
| **Backpressure** | every desired behavior becomes a gate, test, eval, schema, reviewer, approval, or loop condition | Zod outputs, `bunx smithers-orchestrator eval`, `<ReviewLoop>`, `<Approval>`, traces |

The first four shape what the agent *can* do; **backpressure** decides whether the
work is allowed to move forward. A workflow that just "tries its best and moves on"
has no backpressure, and that is where unreliable agents come from.

```mermaid
flowchart TB
  subgraph SHAPE[What the agent CAN do]
    direction TB
    P[Prompt engineering] --> C[Context engineering] --> H[Harness engineering] --> W[Workflow engineering]
  end
  SHAPE --> B{Backpressure<br/>is the work allowed to move forward?}
  B -- gate passes --> F[Forward]
  B -- gate fails --> SHAPE
  style B fill:#fef,stroke:#a3a
  style F fill:#dfe,stroke:#3a3
```

## Backpressure, concretely

Turn each success criterion into a verification signal, and pick the Smithers
primitive that enforces it:

- **Schema**: the step must return a shape: a Zod `output={...}` on the `<Task>`.
- **Test**: generated code must pass: a function task shelling out to `repoCommands.test`.
- **Eval**: an answer must satisfy examples/rubrics: `bunx smithers-orchestrator eval` + scorers.
- **Review**: another agent (or human) must approve: `<ReviewLoop>` / `<Panel>`.
- **Approval**: a human signs off before a risky action: `<Approval>`.
- **Dependency**: step B can't start until step A produced a field: gate on `ctx.outputMaybe(...)`.
- **Trace**: tool calls, retries, and handoffs must be visible: observability + `bunx smithers-orchestrator events`.

Loop until the gate passes (`<Loop>` / `<Ralph until={…}>`) rather than running once
and hoping.

Command gates should classify the failure evidence before they mark work red. A
typecheck is a code failure when `tsc --noEmit` reports TypeScript diagnostics such
as `error TS...`; a nonzero exit, signal, OOM, or timeout with no diagnostics is an
infrastructure failure and should be retried with more headroom before surfacing as
retryable. Test gates follow the same rule: failed tests are red, but a crashed or
timed-out runner with no failed-test report is transient infrastructure, not proof
the patch is bad.

## Sequence for reversibility; isolate the irreversible

Order the work so the reversible, low-stakes steps run first and the irreversible
side effect runs last, behind a gate. Everything before that point is safe to retry,
replay, or throw away. The wire transfer, the production deploy, the email to every
customer: those are the steps you cannot take back, so those are the steps you push
to the end and guard.

The sharper rule is to split the decision from the act. An agent that both decides to
send money and sends it has fused a reversible step, deciding, with an irreversible
one, sending, and you can no longer review or rerun the first without risking the
second. So do not let the agent send the money. Have it return a typed decision, and
make the payout its own downstream task behind an approval gate:

```tsx
// Reversible: cheap to review, safe to retry, replays deterministically.
<Task id="decide-payout" output={outputs.payout} agent={analyst}>
  Should we release the vendor payout? Return shouldPay, amount, and a reason.
</Task>;

// Irreversible: its own task, the only step that touches money, gated by a human.
const payout = ctx.outputMaybe(outputs.payout, { nodeId: "decide-payout" });
payout?.shouldPay && (
  <Sequence>
    <Approval
      id="payout-gate"
      output={outputs.payoutGate}
      request={{ title: `Release $${payout.amount}?`, summary: payout.reason }}
    />
    <Task id="send-payout" output={outputs.receipt} agent={treasury}>
      Wire ${payout.amount} to the vendor, keyed for idempotency.
    </Task>
  </Sequence>
);
```

This buys four things the fused version cannot. The decision is a typed row you can
read, score, and replay. The act is its own node in the graph, so it shows up in
traces and is something a human can approve in isolation. The approval gate puts a
person on the exact amount before any money moves. And the side effect stays
idempotent on retry, because the tool it calls is marked `sideEffect: true` and keyed
with `ctx.idempotencyKey` (see [Mark side-effecting tools and key
them](/guides/common-footguns#mark-side-effecting-tools-and-key-them)). It is the
same move a database makes: do all the work that can roll back, then commit once, at
the end.

## A model call is stateless; context is the only control surface

For a fixed model, output quality is a function of one input: the context window
you hand it. The model keeps nothing between calls. Each call starts from zero and
reads only what you put in front of it. So "get a better result" means "manufacture
a better context window", and an agent is a loop that does exactly that, over and
over.

Every tool an agent runs is one of three context moves:

1. **Delete incorrect context.** This is the worst kind to leave in. A wrong fact
   or a dead path is a false anchor: the model keeps rationalizing toward it and
   tunnels. If you must keep a failed attempt, distill it to one line ("tried X,
   failed because Y, do not repeat") and drop the rest.
2. **Add missing context.** This is what tools are for. A test run, a diff, a stack
   trace, a file read: each turns an unknown into real tokens the model can reason
   over. An agent without tools guesses. An agent with tools looks.
3. **Remove useless context.** Residue is a tax. The output of a finished task is
   adversarial noise for the next one. The rule of thumb: if you could `/clear`,
   you should `/clear`.

Compression scales all three at once. A good summary deletes the wrong, keeps the
missing, and discards the useless in a single pass.

## Three levers, and they trade off

There are three things you can optimize, and pushing one usually costs another.
Naming them keeps you honest about which one you are spending on.

- **Quality.** More attempts, more model diversity, more verification. Three
  planners beat one. A review loop beats a single pass.
- **Cost.** Cheaper models wherever an eval proves they are good enough. The work
  is proving it, then promoting the cheap model on the strength of the score.
- **Speed.** Parallelism, and refusing to block fast work behind a slow sibling.

Smithers gives you a primitive per lever. `<Panel>` and `<ReviewLoop>` buy quality.
`<Sidecar>` buys cost: it runs a cheap shadow model next to the primary task,
scores both with the same scorer, and reports the delta without ever touching the
result, so over time you see when the cheap model is ready to promote. `<Parallel>`
buys speed.

The quality lever has a worked example in this repo:
[`examples/swe-evo/workflow/swe-evo-panel.tsx`](https://github.com/smithersai/smithers/blob/main/examples/swe-evo/workflow/swe-evo-panel.tsx).
It plans with a model-diverse `<Panel strategy="synthesize">` (three planners draft
independent plans, a moderator synthesizes one), then implements inside a
`<ReviewLoop>` that loops until the reviewers approve. The reviewer approval is a
proxy for the hidden test suite, so the loop climbs toward a signal it cannot see
directly.

## Hill climbing, two hills

An agent improves by climbing. There are two hills, and the second one pays more.

The obvious hill is the **output**: generate, critique, regenerate. Write the code,
run the reviewer, fix what the reviewer flagged. `<ReviewLoop>` and `<Optimizer>`
make this loop durable: each pass is a persisted frame, so a crash resumes mid-climb
instead of restarting.

The higher-leverage hill is the **context**. Before the next attempt, ask "what
information would make this attempt obviously better?" and go get it. A failing
test, the actual file instead of a guess about it, a synthesized plan instead of a
cold start. The swe-evo panel climbs both at once: the panel manufactures a better
context (a synthesized plan) before the first line of code, and the review loop
climbs the output after.

## The smart zone

Agents perform best under about 200k tokens of context, and noticeably better under
100k. Past that, attention thins and quality drops. So the move is to give an agent
a goal it can finish inside that budget, with the research and planning already
done, so it spends its window on the work instead of on discovery. This is why a
research step and a plan step precede implementation: they keep the implementer in
the smart zone.

Smithers measures the zone so you are not guessing:

- `smithers.tokens.context_window_per_call` is a histogram of per-call context
  size, bucketed at exactly `[50k, 100k, 200k, 500k, 1M]`.
- `smithers.tokens.context_window_bucket_total` is a counter of how many calls
  landed in each bucket, so you can see drift toward the large buckets.
- Per-node usage shows in `bunx smithers-orchestrator node`, and live as the
  `TokenUsageReported` event (the 🧮 line in the event stream).

The in-workflow guardrail is `<Aspects tokenBudget>`. It is
enforced at task dispatch: before each descendant task runs, the engine compares the
run's accumulated token total against `max` and applies `onExceeded` (`fail` raises
`ASPECT_BUDGET_EXCEEDED`, `warn` logs and continues, `skip-remaining` skips the
task). A budget breach is a real, catchable error, which is what makes the durable
`/clear` below possible.

## Plan the validation, not the feature

Your scarce resource is deciding how you will *know* it worked. Spend it where it is
cheapest.

Different pipeline stages cost wildly different amounts to review. Vibe-checking a
finished output is near free, and it lets debt pile up unseen. Reading a 500-line
diff is miserable and you will skim it. Reading a *plan* is cheap and high leverage:
a page of intent, before any code exists. So put your eyeballs where they are
cheapest. Review the plan, then test the output, and skip the diff.

This only works with two things in place:

- **Plans with teeth.** The plan names the tests, the acceptance criteria, and the
  machine-checkable definition of done. A plan that says "implement the feature" has
  no teeth and gates nothing.
- **Real backpressure.** Tests, CI, and types push back on the agent directly. The
  agent feels resistance from the toolchain, not from you squinting at a diff at
  11pm.

The payoff is large. A complex feature attempted cold one-shots maybe 40% of the
time. The same feature, preceded by a vetted plan with teeth and backed by real
gates, one-shots around 98%.

## Goal-based over ambiguous tasks

Write tasks around validation criteria, and leave the implementation details out,
unless a planning step already worked them out (then pass them down to save the
implementer's context). Measurable goals are best: "the suite passes", "the score
clears 0.9", "the schema validates". When a goal is genuinely fuzzy, the goal can be
"a reviewer approves", and you should prefer an agent reviewer over a human one so
the loop stays autonomous.

The validation prompt deserves as much thought as the work prompt. A sloppy reviewer
prompt is a broken feedback channel, and the agent will happily climb toward the
wrong summit.

## Observability is non-negotiable

An agent must always be able to self-validate and debug. It needs a test signal, a
trace, a real error to read. When that channel is missing or broken, treat it as a
fatal condition: stop and fix the channel. Do not keep optimizing against a phantom
signal, because the agent will produce confident work that satisfies a metric you
cannot trust. When you build a feature, invest in the observability that the next
agent will need to debug it.

## The testing bar is higher for agentic code

Never consider a feature working without an end-to-end test that proves it. E2e
tests are the bread and butter here, because an agent that cannot run the whole path
cannot tell whether it is done. Unit tests still earn their place (TDD works well on
small, self-contained snippets), but the e2e test is the one that closes the loop.

A direct consequence is to build in **vertical slices**: one feature working end to
end, then the next. A horizontal slice (a whole service layer for every feature at
once) has nothing to validate until the very end, which is exactly when you want
validation to have been happening all along.

## Attention is finite; delegate the periphery

Keep linters, style guides, commit-message crafting, and the rest of the periphery
out of the primary agent's attention. Push them to cheaper models in separate passes
with fresh, clean context. For version control, lean on the automatic jj snapshotting
Smithers already does instead of spending agent attention on git mechanics.

This generalizes into **sandwich delegation**: smart, expensive agents plan and
review at the two ends, while cheaper agents implement in the middle. Recurse as the
work grows. A capable model can write a Smithers script whose `<Panel>` uses two
strong models to plan and whose `<ReviewLoop>` validates, while cheaper models do the
implementation in between. The more cost-insensitive you are, the more of the middle
you can hand to a strong implementer. Do not spend your most expensive model on work
a cheaper one can do unless you have a reason to.

## The lifeline rule: protect the orchestrator's own context

The orchestrator driving a long run is the one context that lives for the whole
job. Every sub-agent is disposable; the orchestrator is not. That makes its
context window the scarce resource, and the failure mode is quiet: it reads a
4,000-line diff to "check the work," ingests a run's full event log to debug,
opens three candidate branches to pick a winner, and now every downstream
decision is made from a window full of residue.

So never read large material into the orchestrator's own context. Spawn a
throwaway sub-agent to read the diff, the log, or the file and hand back one
paragraph. Judging is a read too: to pick the best of N candidates, spawn a
fresh verifier that ranks them and returns a verdict, rather than pulling N
diffs into your window; the agent that has to stay clean should not also be the
one holding every artifact. `<ReviewLoop>`, `<Panel>`, and `<ScanFixVerify>`
exist so the verification lives in a separate context from the thing being
verified. Keep the orchestrator lean and it can run all day; pollute it and the
whole job degrades from the top down.

## Re-read your instructions to fight drift

A long session drifts from its instructions the same way it drifts out of the
smart zone. Fifty turns in, the goal has blurred, the plan has a dozen
amendments, and the operating rules you started with have quietly stopped being
followed: you reach for the expensive model on cheap work, call a diagnosis
"done," let scope creep in. The residue does not announce itself.

The cheap fix is to re-read. Every few steps of a long job, and always after a
`<ContinueAsNew>` handoff, re-read the spec or goal you are working to and this
doctrine, then check your recent behavior against them: right model tier,
evidence bar actually enforced, still solving the stated problem. Drift you
catch yourself costs a re-read; drift the human catches costs a day. This is why
durable `/clear` re-injects the distilled goal on every fresh window: a clean
context that has forgotten its instructions is only half the fix.

## POC in the planning phase

A throwaway proof of concept is a fast way to surface the ideas a plan needs. A POC
optimizes for speed and cost, never for quality: you are going to discard it. What
survives is the lessons, and those feed the plan. Treating the POC as production
code is the trap. Build it, learn from it, delete it, then plan.

## Don't over-granularize

Splitting a goal into a dozen babysat micro-tasks is micromanagement, and it costs
you the agent's own judgment about how to get there. Give an agent a goal it can
achieve inside the smart zone and let it figure out the how. When the goal is too big
for one window, orchestrate several agents toward it rather than scripting every
step of one. Task size scales with agent power: a weaker model takes a smaller bite,
a strong model takes a larger one.

## Durable "/clear": a context handoff

A long-running loop accumulates context the way a chat session does. Once it drifts
out of the smart zone, every later turn gets worse. The fix a human does by hand is
`/clear`: drop the residue, keep the few facts that still matter, start fresh.

You can make that automatic and durable by composing three primitives:

- `<Aspects tokenBudget>` sets a hard ceiling on the loop's context. A breach throws
  `ASPECT_BUDGET_EXCEEDED`.
- `<TryCatchFinally catchErrors={["ASPECT_BUDGET_EXCEEDED"]}>` catches exactly that
  code instead of failing the run.
- The catch branch renders `<ContinueAsNew state={...} />`, which closes the current
  run and opens a fresh one carrying only the distilled state, back inside the smart
  zone with no residue.

The code below is the real
[`examples/context-handoff/workflow.tsx`](https://github.com/smithersai/smithers/blob/main/examples/context-handoff/workflow.tsx),
so `bunx smithers-orchestrator graph examples/context-handoff/workflow.tsx --input '{}'`
renders its graph (including the catch branch) and `check-docs` verifies every import
here against the real package facade. That runnable file is the anti-rot teeth: if the
API drifts, the graph render and the typecheck fail.

```tsx
/**
 * Durable "/clear": a context handoff.
 *
 * Agents perform best in the smart zone (under ~200k tokens of context, ideally
 * under ~100k). A long-running loop accumulates context the way a chat session
 * does, and once it drifts out of the smart zone every later turn gets worse.
 * The fix a human does by hand is `/clear`: drop the accumulated residue, keep
 * the few facts that still matter, start fresh.
 *
 * This workflow does that automatically and durably. The pieces:
 *
 *   <Aspects tokenBudget>        a hard token ceiling for the subtree. The
 *                                engine enforces it at task dispatch; a breach
 *                                throws ASPECT_BUDGET_EXCEEDED.
 *   <TryCatchFinally>            an error boundary that catches exactly that
 *                                code and renders the catch branch instead of
 *                                failing the run.
 *   <ContinueAsNew state={...}>  the catch branch. It closes this run and opens
 *                                a fresh one carrying ONLY the distilled state,
 *                                so the new run starts back inside the smart
 *                                zone with no residue.
 *   <Loop>                       the little while loop that does the work. Each
 *                                pass makes one increment of progress until the
 *                                goal is met.
 *
 * Run the graph without executing it:
 *
 *   bunx smithers-orchestrator graph examples/context-handoff/workflow.tsx --input '{}'
 *
 * The DAG includes the catch branch, so the render proves the whole handoff
 * wiring compiles.
 */

import { createSmithers, ClaudeCodeAgent } from "smithers-orchestrator";
// In-repo, "smithers-orchestrator" resolves to a limited examples entry that
// does not re-export <Aspects>/<TryCatchFinally>; import them from the
// components package directly. End-user code can import both from
// "smithers-orchestrator".
import { Aspects, TryCatchFinally } from "@smithers-orchestrator/components";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod/v4";

const here = dirname(fileURLToPath(import.meta.url));

/** The minimal context we carry across a handoff. This, and only this, is what
 *  survives a `/clear`: the goal, which generation we are on, the last summary,
 *  and a short list of durable learnings (distilled wrong paths, not raw logs). */
type DistilledState = {
  goal: string;
  generation: number;
  lastSummary: string;
  learnings: string[];
};

export const schemas = {
  // One increment of work. `learnings` are distilled facts worth carrying
  // forward ("tried X, failed because Y"); `done` ends the loop.
  step: z.object({
    summary: z.string().default(""),
    learnings: z.array(z.string()).default([]),
    done: z.boolean().default(false),
  }),
};

// A local, gitignored DB next to this file so `smithers graph` never touches
// the project's smithers.db.
const api = createSmithers(schemas, { dbPath: join(here, "smithers.db") });
const { smithers, Workflow, Task, Loop, ContinueAsNew, outputs } = api;

// Autonomous agent: bypass flags on, no pinned cwd (that would override a
// <Worktree>). Graph rendering does not run the agent; these are the real
// flags a live run needs.
const worker = new ClaudeCodeAgent({
  model: "claude-opus-4-8",
  permissionMode: "bypassPermissions",
  dangerouslySkipPermissions: true,
});

const MAX_CONTEXT_TOKENS = 150_000;

export default smithers((ctx) => {
  // ctx.input fields arrive raw-or-null, so coalesce every read. The carried
  // state arrives under the continuation envelope on a handoff; on a cold start
  // it is absent and we read the top-level goal instead.
  const input = (ctx.input ?? {}) as {
    goal?: string | null;
    __smithersContinuation?: { payload?: Partial<DistilledState> | null } | null;
  };
  const carried = input.__smithersContinuation?.payload ?? null;
  const goal = carried?.goal ?? input.goal ?? "Make the failing test suite pass.";
  const generation = (carried?.generation ?? 0) + 1;

  // Read this generation's progress out of typed outputs (empty on a fresh
  // render). The loop is done when the last step says so.
  const steps = ctx.outputs.step ?? [];
  const lastStep = steps[steps.length - 1];
  const done = lastStep?.done === true;

  // Distill the state we would hand off: the goal, the next generation number,
  // the latest summary, and the last 10 learnings. Capped on purpose, so the
  // fresh run starts small and back inside the smart zone.
  const distilled: DistilledState = {
    goal,
    generation,
    lastSummary: lastStep?.summary ?? carried?.lastSummary ?? "",
    learnings: [...(carried?.learnings ?? []), ...(lastStep?.learnings ?? [])].slice(-10),
  };

  const prompt = [
    `Goal: ${goal}`,
    carried
      ? `Fresh context, generation ${generation}. Prior summary: ${distilled.lastSummary || "(none)"}.`
      : "Fresh start.",
    distilled.learnings.length
      ? `Known so far:\n${distilled.learnings.map((l) => `- ${l}`).join("\n")}`
      : "",
    "Make one increment of progress. Report a short summary, any durable learnings (distill wrong paths to 'tried X, failed because Y'), and set done=true only when the goal is fully met.",
  ]
    .filter(Boolean)
    .join("\n\n");

  return (
    <Workflow name="context-handoff">
      <Aspects tokenBudget={{ max: MAX_CONTEXT_TOKENS, onExceeded: "fail" }}>
        <TryCatchFinally
          catchErrors={["ASPECT_BUDGET_EXCEEDED"]}
          catch={<ContinueAsNew state={distilled} />}
          try={
            <Loop id="work" until={done} maxIterations={50}>
              <Task id="step" output={outputs.step} agent={worker}>
                {prompt}
              </Task>
            </Loop>
          }
        />
      </Aspects>
    </Workflow>
  );
});
```

## Smithers does the context engineering for you

You should not need to know any of the above to get a workflow. The
`create-workflow` workflow is the entry point to the
"context engineering for you" layer:

```bash
bunx smithers-orchestrator workflow run create-workflow \
  --prompt "Watch a landing request and auto-land it once CI is green"
```

It clarifies your ask into a spec, **provisions the docs and skills the work needs**
(pulls the relevant `llms-*.txt`, finds the closest `examples/` template, and
installs worker skills via `bunx smithers-orchestrator skills add`), designs the graph, pauses for
your approval, scaffolds the files, verifies the graph renders, and documents the
result. You answer product questions; it produces the prompts, context, components,
and gates.

This is the direction Smithers is heading: a concierge that takes a vague script,
interrogates it, routes it to the right skills and workflows, adds backpressure, runs
as much as it can, and reports legibly. The durable, observable, gated workflow
is something you *describe* rather than hand-build.

## Further reading

The field this builds on: Anthropic and OpenAI on prompting and on evaluating the
model *and* the harness together; LangChain and LlamaIndex on context engineering;
HumanLayer on harness engineering for coding agents; the Ralph loop on
acceptance-driven, fresh-context iteration; and BAML on treating structured output
as schema engineering.

---

## Agent Operating Playbook

> How an AI harness should translate human requests into Smithers workflows, verification, observability, and evidence reports.

This page is for the AI agent operating Smithers on a human's behalf.

It belongs to the **For Agents** docs set. The human-facing docs are the
**For Humans** Guide, starting at What Smithers Is.
Agents should read this page and the generated `llms.txt` /
`llms-full.txt` bundles before driving Smithers for a user.

The human does not use Smithers by memorizing CLI commands or authoring `.tsx`
workflows. The human talks to you. You decide when Smithers is the right tool,
you run the commands, you watch the run, you ask for account-gated decisions,
and you return a clear report with evidence.

If you remember one rule, remember this:

> Do not ask the human to run Smithers commands. The human's job is to state the
> outcome, answer product questions, approve gates, and provide credentials or
> account access when needed. Your job is to operate the harness.

And one more rule that is just as important:

> You are an **orchestrator, not an implementer.** Do the background work
> *through Smithers*, not through your own ad-hoc subagents. For anything
> long-running, multi-step, retryable, or run-while-the-human-is-away, launch a
> Smithers workflow. Smithers spawns the worker agents and persists every step.
> Spend your time observing the run, clearing gates, and reporting. If you want
> parallel help, point your own subagents at *monitoring* the Smithers run
> (tailing events, summarizing, flagging gates), never at re-doing the work a
> workflow should own. The moment you're tempted to spawn a subagent to "go
> build/fix/research this in the background," that is the signal to run a
> workflow instead.

And one rule about the operator boundary:

> **The workspace Gateway is the run control plane.** A controller, Bun cron
> job, monitor, bot, or custom client must use
> `smithers-orchestrator/gateway-client` or Gateway RPC/REST for run discovery,
> health, events, launch/resume/cancel, approvals, signals, scores, and node
> output. Never open SQLite, PGlite, or Postgres from an operator script; never
> query `_smithers_*`; never import `openSmithersStore` or CLI-internal
> `findAndOpenDb`; and never probe stores by adding `--backend` to `ps` or
> `inspect`. Direct store access is reserved for runtime implementation,
> migration, and maintainer diagnostics. One-shot public CLI commands are fine;
> durable automation goes through the Gateway.

And a rule about how hard to push before involving the human:

> **Drive workflows to completion; fix what you can yourself.** When a run is
> launched to run to completion (especially "in the background" or while the
> human is away), it is your job to make it succeed end-to-end. Try your hardest.
> If the run fails or stalls on anything you are capable of fixing (a bad model
> id, a non-executable binary, a stale/polluted baseline, an over-strict or
> mis-wired gate, a verdict that is written to disk but not captured into the
> workflow's done-check, a bug in a *generated* workflow), **fix it and resume
> the run.** Keep a keeper/supervisor loop alive so the run survives owner-exits,
> and re-run after each fix. Only stop and involve the human for (a) an explicit
> human **approval gate** in the workflow, or (b) something **only a human can
> do** (credentials, account access, irreversible outward actions). **Do not hand
> a self-fixable issue back to the human and wait.** A human who said "run it to
> completion" and walked away expects a finished result, not a report asking
> permission to apply an obvious fix. Diagnosing the problem is not the finish
> line; a working run is.

## The operating loop

Use this loop for broad, ambiguous, risky, long-running, or multi-agent work:

1. Capture the word barf. Let the human describe the outcome in messy language.
2. Grill for missing context. Ask focused questions only when the answer cannot
   be discovered safely from the repo, docs, services, or prior artifacts.
3. Convert the request into a goal-based spec. Define done, non-goals,
   acceptance criteria, risks, and the evidence the human needs to see.
4. Design the Smithers run. Decide the workflow, agents, gates, retry loops,
   observability, assumption tests, and report artifacts before you start.
5. Validate the workflow shape. Render the graph with
   `bunx smithers-orchestrator graph <workflow>` (there is no `up --dry-run`; the
   `graph` verb is the dry-run path, rendering the graph without executing or
   persisting anything) or dry-run evals before launching expensive or
   destructive work.
6. Run with observability. Use hot reload while authoring, inspect the run while
   it executes, and suggest the UI when a visual state would help the human.
7. Report with evidence. Produce a concise Markdown or HTML report that links to
   outputs, tests, traces, screenshots, GIFs, and the run ID.

This is the "make a harness that makes the app" pattern: the first deliverable
is not just code. It is a durable system that can plan, build, verify, observe,
and explain the code.

## Translate human prompts into Smithers work

| Human prompt | What you should do |
| --- | --- |
| "Build this product idea start to finish. I have thoughts but not a spec." | Run an interview or `grill-me` flow first. Produce a product spec, design spec, engineering spec, and acceptance criteria. Add a gate before implementation. |
| "Add rate limiting and don't stop until it is production-ready." | Run an implementation workflow with a test and review loop. Define production-ready as passing tests, review approval, docs updates, and an evidence report. |
| "Figure out whether Privy server wallets can deposit into a Morpho vault on Tempo." | Treat it as an assumption-probe workflow. Write a tiny reproducible test against testnet or documented APIs before any product work depends on it. Report exact evidence and remaining unknowns. |
| "Make the UI look like the design and show me it actually works." | Build the UI, run browser or simulator checks, capture screenshots or GIFs for each important screen, then ask an independent reviewer agent to compare against the design language. |
| "Keep working on flaky tests while I am away." | Start a durable loop such as `ralph`, `debug`, or a local workflow with a clear cap or cancellation path. Monitor progress, summarize failures, and stop only when the finish line is reached or the cap is hit. |
| "Migrate this subsystem, but show me the plan first." | Run research and planning first, then pause on an approval gate. After approval, execute milestones in worktrees and merge only validated chunks. |
| "Something went wrong in the run. What happened?" | Run `why`, inspect events and node output, summarize the blocker, propose options, and continue operating. Do not ask the human to debug from the terminal. |

When the user gives a narrow one-shot request, Smithers may be unnecessary. But
if the task has phases, risk, loops, approvals, third-party dependencies,
parallel work, or a need to run while the human is away, use a workflow.

## Context engineering

Context engineering is the work of turning a vague request into a runnable,
auditable job.

Start by writing down:

- Outcome: what should exist when the run is done.
- Finish line: how you will know the work is done.
- Evidence: what the human needs to see to trust the result.
- Constraints: files, platforms, budgets, style, deadlines, and non-goals.
- Unknowns: assumptions that must be proven before you build on them.

Then gather context before executing:

- Read repo docs, README files, package scripts, tests, issue trackers, design
  docs, and previous Smithers outputs.
- Inspect relevant source files and architecture before making a plan.
- Read third-party docs or APIs when behavior could have changed.
- Prefer small probes over confident guesses for external services.
- Store the resulting spec somewhere durable, such as `.smithers/specs/`,
  `docs/`, or an artifact directory, so later agents can consume it.

Good Smithers prompts are goal-based, not instruction soup:

```text
Implement account-level rate limiting for API routes.

Finish line:
- Existing tests pass.
- New tests prove per-account and per-IP limits.
- The review approves the diff.
- The final report explains changed files, behavior, and rollout risks.

Verification:
- Run lint/typecheck/unit tests.
- Add an assumption test if the existing rate-limit library behavior is unclear.
- Capture failure output and feed it back into the next iteration.
```

Use explicit stop conditions. "Keep going until tests pass" should also carry a
cap, a fallback, and a report path. Infinite effort is not a finish line.

## Backpressure verification

Backpressure means the workflow pushes evidence back against the agent's claim
that the task is done. Do not accept "looks good" as verification. Encode checks
that can fail.

Use these Smithers patterns:

- `<CheckSuite>` for parallel command or agent checks with one pass/fail verdict.
- `<ScanFixVerify>` for scan -> fix -> verify -> report loops.
- `<ReviewLoop>` or `<LoopUntilScored>` when the exit condition is reviewer
  approval or a score threshold.
- Eval suites for repeatable workflow-level regressions with JSON reports.
- Task scorers for telemetry such as schema adherence, faithfulness, relevance,
  latency, and custom LLM-judge checks.

A strong run defines backpressure before execution:

```text
Before implementing:
- Identify which tests should fail before the fix.
- Add or update the smallest regression test that proves the behavior.
- Define an independent reviewer prompt that can reject the diff.
- Define a report schema: changed files, commands run, failures, fixes, evidence.
```

Backpressure should be independent where possible. The agent that wrote the code
should not be the only judge. Use a second reviewer agent, command-based tests,
eval cases, or real service probes.

## Assumption tests

Assumption tests are small probes that prove third-party libraries, APIs, cloud
services, entitlements, or chains behave the way the plan assumes.

Write them before the main build when the assumption is expensive to unwind.

Examples:

| Assumption | Probe before building on it |
| --- | --- |
| "This SDK supports the chain we need." | Write a tiny script that imports the SDK, constructs the target chain, reads a known contract, and records the result. |
| "The testnet faucet funds the account we will use." | Generate a throwaway address, call the faucet or RPC method, poll balance, and save the transaction or response. |
| "A vault exists with real liquidity." | Query the vault contract or API, check assets, total assets, curator identity, deposit limits, and share math. |
| "The mobile entitlement allows this alarm behavior." | Build the smallest native sample or simulator test that schedules and observes the alarm path. |
| "The payment provider gives us idempotent retries." | Run a local or sandbox integration test that retries the same idempotency key and proves no duplicate charge path. |
| "The media API can generate the assets we need." | Call the sandbox API with one prompt, validate format, duration, latency, and failure handling, then store the output. |

Keep assumption probes narrow. They should answer one question and produce
evidence. If the probe fails, report that the product plan must change before
implementation continues.

## Observability-first runs

If you cannot see the run, you cannot operate it well.

For local and development work, use the CLI surfaces yourself:

```bash
bunx smithers-orchestrator ps
bunx smithers-orchestrator inspect RUN_ID --watch
bunx smithers-orchestrator events RUN_ID --watch
bunx smithers-orchestrator node NODE_ID --runId RUN_ID
bunx smithers-orchestrator scores RUN_ID
bunx smithers-orchestrator why RUN_ID
```

For any long-lived observer or controller, start from the workspace singleton
and use its typed API:

```bash
bunx smithers-orchestrator gateway status --format json
```

The status response provides the verified `url` for the current workspace. If no
singleton is running, start `bunx smithers-orchestrator gateway` under the
controller's service manager, then create a `SmithersGatewayClient` with that
URL. Do not assume port 7331 and do not parse the Gateway runtime state file;
`gateway status` performs workspace and process identity verification for you.
Use `getRun`/`listRuns` for snapshots and `streamRunEventsResilient` for live
health instead of polling storage files.

If Gateway startup reports `SMITHERS_MIGRATION_REQUIRED`, stop there and perform
the explicit `smithers migrate` operation (after preserving the legacy store),
then restart the Gateway. Do not delete the database, pin a different backend in
a monitoring script, or pass `--backend` to read/control commands to make the
error disappear. Backend selection is a Gateway boot/deployment concern; once
the Gateway is healthy, every operator uses the same API regardless of whether
the store behind it is SQLite, PGlite, or Postgres.

Use serve mode when you need HTTP status, SSE events, remote approvals, or
Prometheus metrics:

```bash
bunx smithers-orchestrator up workflow.tsx --serve --metrics --port 7331
```

Use the observability stack when the work needs traces, metrics, dashboards, or
a reviewer evidence bundle:

```bash
bunx smithers-orchestrator observability
```

Enable OpenTelemetry export when you need trace-level proof, then include the
Grafana, Loki, Tempo, or Prometheus links and query results in the final report.
For debugging, correlate run ID, node ID, attempt, event stream, agent trace,
and any application logs.

When the human would benefit from seeing the work, suggest the UI and operate it
for them:

- `bunx smithers-orchestrator monitor [RUN_ID]` opens the Smithers Monitor: a
  zero-setup live view over every run in the workspace (grouped runs, execution
  tree, event log, approvals inbox), optionally focused on one run. It observes
  only; it launches nothing.
- `bunx smithers-orchestrator gui <path>` opens the workspace view.
- `bunx smithers-orchestrator ui RUN_ID` opens a workflow custom UI when the Gateway is running
  and the workflow has a registered UI.
- Gateway and custom UI streams expose run state, frames, approvals, node output,
  and DevTools snapshots for richer visual monitoring.

Phrase this as: "I can open the Smithers UI for this run so you can watch the
plan, gates, and evidence live." Do not phrase it as homework for the human.

## Hot validation loop

Use hot mode while authoring or tuning a workflow:

```bash
bunx smithers-orchestrator graph workflow.tsx
bunx smithers-orchestrator up workflow.tsx --hot true --input '{"prompt":"..."}'
```

The graph command validates the rendered shape without executing the whole job.
Hot mode lets workflow and prompt edits apply on the next render frame while
finished tasks stay persisted.

Rules of thumb:

- Use `--hot true` for prompt wording, task body, and non-schema workflow edits.
- Restart fresh when output schemas or task ID shapes change.
- Keep task IDs stable and data-derived so resume and hot reload can preserve
  completed work.
- After a hot edit, inspect the graph or next frame to confirm the workflow now
  does what you intended.

Do not treat hot reload as magic. Validate that the new frame mounted the right
tasks, the old completed tasks stayed completed, and any changed prompt actually
reached the next agent attempt.

## Reports for the human

End every substantial Smithers run with a human-readable report. Markdown is
fine; HTML is better when screenshots, GIFs, traces, or tables make the result
clearer. Write it as an artifact, for example:

```text
artifacts/smithers-report.md
artifacts/smithers-report.html
artifacts/screenshots/
artifacts/gifs/
artifacts/evals/
artifacts/traces/
```

The report should include:

- Summary: what changed, what shipped, and what did not.
- Run metadata: workflow name, run ID, branch or worktree, key node IDs.
- Prompt and spec: the interpreted goal, acceptance criteria, and non-goals.
- Verification: commands, tests, evals, scorers, reviewer verdicts, and failures.
- Assumption tests: probes run, outputs captured, and open risks.
- Observability: event excerpts, metrics/traces, logs, screenshots of dashboards.
- Visual evidence: screenshots, GIFs per major screen, and walkthrough video for
  UI or product work.
- Human decisions: approvals requested, decisions made, and remaining gates.
- Next steps: exact options, tradeoffs, and what you recommend.

For UI work, the minimum visual report is screenshots for each important state.
The stronger report includes GIFs for interactions and a walkthrough video that
clicks through every user-visible flow. If you cannot capture visuals, say why
and include the command or environment blocker you observed.

## Failure protocol

When a run fails or pauses unexpectedly, stay in the operator role:

1. Inspect the run with `why`, `inspect`, `events`, `node`, and logs.
2. Identify whether the blocker is code, tests, credentials, an approval gate,
   a third-party service, rate limits, missing context, or a workflow bug.
3. If it is fixable by you, fix it or resume from the correct frame.
4. If it needs the human, ask for the smallest decision or credential needed.
5. Report what happened, what evidence supports that diagnosis, and what you
   are doing next.

Bad response:

```text
Run smithers inspect and tell me what it says.
```

Good response:

```text
The run is paused at the deployment approval gate. I inspected the node output:
tests passed, the review approved, and the only remaining action is your
approval to deploy. I recommend approving because the diff is limited to the
rate-limit middleware and the rollback path is unchanged.
```

The human should feel like they are talking to a careful operator, not like they
were handed a control plane manual.

## Minimal checklist

Before launching:

- Outcome, finish line, and evidence are written down.
- Missing context has been researched or asked for.
- Third-party assumptions have probes or are explicitly marked as risks.
- Workflow graph (`bunx smithers-orchestrator graph <workflow>`, the dry-run
  path) or eval dry-run has been checked.
- Backpressure checks exist and can fail.
- Observability path is chosen.
- Report artifact path is chosen.

While running:

- Watch the run.
- Use the UI when visual state, approvals, or steering would help.
- Feed failures back into the workflow instead of manually papering over them.
- Keep the human updated in plain English.

Before closing:

- Regenerate or collect the final evidence.
- Write the report.
- Include screenshots, GIFs, videos, logs, traces, eval reports, and reviewer
  verdicts when they exist.
- Explain remaining risk honestly.
- Commit or open the review artifact only after verification is complete.

---

## JSX API

> Author workflows as JSX trees. Smithers renders the tree, dispatches ready tasks, persists outputs, and re-renders. Branching and parallelism are plain JSX conditionals.

Workflows are JSX trees. Smithers renders the tree, extracts ready tasks, executes them, persists their outputs, and re-renders. Branching, looping, and parallelism are normal JSX.

<Note>API reference: Authoring and Components list every authoring helper and JSX element, their options, and links to source and tests.</Note>

## Setup

Most projects should use `bunx smithers-orchestrator init`; it scaffolds everything below.

To embed into an existing codebase:

```bash
bun add smithers-orchestrator zod
bun add -d typescript @types/react @types/node
```

`smithers-orchestrator` bundles React and ships the JSX runtime as
`smithers-orchestrator/jsx-runtime`, so you do **not** add `react` or `react-dom`
yourself. `jsxImportSource` (below) routes JSX through the workflow reconciler,
never React DOM. The one required dev dep is `@types/react` (React ships no
bundled types, and the JSX transform resolves its JSX namespace from it). The
browser UI surface (`smithers-orchestrator/gateway-react`) is the only place that
takes `react`/`react-dom` as peers.

Minimal `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "smithers-orchestrator",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
```

`jsxImportSource` is the only non-standard line; it routes JSX through `smithers-orchestrator/jsx-runtime` instead of React DOM.

Optional MDX prompts: add `bun add -d @types/mdx` and a `preload.ts` that calls `mdxPlugin()`, register it in `bunfig.toml` as `preload = ["./preload.ts"]`.

Verify with `bunx tsc --noEmit` and `bunx smithers-orchestrator --help`.

## A minimal workflow

```tsx
// @jsxImportSource smithers-orchestrator (only needed if not set in tsconfig.json)
import { createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  input: z.object({ repo: z.string() }),
  analysis: z.object({ summary: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="analyze">
    <Sequence>
      <Task id="analyze" output={outputs.analysis}>
        {{ summary: `Analyze ${ctx.input.repo}` }}
      </Task>
    </Sequence>
  </Workflow>
));
```

`createSmithers` is a named export. The lowercase `smithers` wrapper is not a
top-level import; it is the property returned by `createSmithers(...)`.
`smithers((ctx) => ...)` returns the `SmithersWorkflow` value to export from the
workflow file. The `input` schema above types `ctx.input.repo`; omit `input` and
`ctx.input` is `unknown`, so you must guard or parse it yourself.

`outputs.analysis` is the typed reference for the Zod schema, so typos are compile errors. The task body is a JSX expression (`{...}`) whose value is a plain object, with no LLM call here, just a static return. Real tasks pass a `run` prop or an AI model. See the Task component reference.

## Reactivity

The tree re-renders on every frame, so branching is a normal JSX conditional. Inside a workflow function, `ctx` exposes `ctx.input` and `ctx.outputMaybe(ref, { nodeId })`. The latter returns the output of a completed task, or `undefined` if it hasn't run yet:
The `ctx` parameter is optional: it is just a normal function argument, so a workflow that never reads `ctx.input` or `ctx.outputMaybe` can drop it entirely and write `smithers(() => ( ... ))`. Add `(ctx)` only when you actually reference dynamic input or prior task output.

```tsx
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
{analysis ? <Task id="report" output={outputs.report} agent={writer}>...</Task> : null}
```

The `report` Task doesn't exist in the plan until `analysis` completes. No placeholder, no skipped node. The conditional IS the dependency. Unlike static DAG tools that require you to declare optional nodes upfront, the JSX conditional is evaluated fresh each frame: if `analysis` is undefined, the `report` task simply doesn't exist in that frame's plan.

For the common case of a task simply consuming one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` children callback expresses the same gating more directly; reach for `ctx.outputMaybe` when the logic is structural (branching on content, loops, counts) rather than plain existence.

## Read next

- Tour: six-step worked example with agents, schemas, approvals, resume.
- How It Works: the render → execute → persist loop.
- Components: full prop surface for every JSX element.

---

## CLI

> Every Smithers CLI command in one structured catalog (TOON format).

Always invoke as `bunx smithers-orchestrator <command>` (see Installation for why). Use `--help` on any command for the canonical option list.

## Conventions

- Run control is workspace-scoped. The workspace Gateway is the default control plane for controllers, Bun cron jobs, monitors, bots, and custom clients; use `smithers-orchestrator/gateway-client` or Gateway RPC/REST rather than opening SQLite/PGlite/Postgres or querying `_smithers_*` tables. One-shot CLI commands are also a supported abstraction boundary. Direct stores are runtime/migration/maintainer-diagnostic internals.
- `smithers gateway status --format json` discovers the verified singleton URL for the current workspace. Do not parse its runtime state file, assume port 7331, or pass `--backend` to `ps`/`inspect`/other control commands to search another store. `--backend` is for Gateway/workflow bootstrap and explicit migration diagnostics only.
- Task root: tools run from the project root (the nearest directory containing a `.smithers/`, walking up from the working directory). `up`, `workflow run`, `graph`, and `eval` all resolve the root this way, so the launch form never changes where tasks run. Override with `--root`; a resume without `--root` reuses the root the run was originally launched with.
- Boolean flags accept either bare form (`--watch`) or explicit `--watch true|false`.
- Global options: `--format toon|json|yaml|md|jsonl`, `--filter-output <key.path>`, `--full-output`, `--token-count`, `--token-limit N`, `--token-offset N`, `--schema`, `--llms`, `--llms-full`, `--mcp`, `--help`, `--version`.
- MCP stdio mode: pass `--mcp` to start Smithers as an MCP server. Add `--surface semantic|raw|both` to choose the exposed tool surface. Add `--allowed-tools name,name` and/or `--read-only` to scope the semantic toolset exposed to outbound MCP clients.
- Workflow resolution: `revert`, `replay`, `fork`, `retry-task`, and `timetravel` take a workflow file path. `up`, `graph`, `eval`, and `optimize` accept either a workflow path or a discovered workflow ID (an existing file is used as-is; otherwise the argument is resolved as an ID, the same way `workflow run` does). `workflow run <name>` resolves IDs from the nearest local `.smithers/workflows/<name>.tsx` (walking up from the working directory) and from the global `~/.smithers/workflows/` pack. Local workflows take precedence: on an id collision the local file wins. The global pack honors `SMITHERS_HOME`. So global workflows run from any directory, while a repo's own pack can override them by name.
- Rewrites: `bunx smithers-orchestrator workflow WORKFLOW_ID` runs a discovered workflow when `<id>` resolves; `bunx smithers-orchestrator workflow.tsx` behaves like `bunx smithers-orchestrator up workflow.tsx`; `bunx smithers-orchestrator chat create` behaves like `bunx smithers-orchestrator chat-create`.
- JSON arguments are preflighted before workflow modules load. `--input` and `--annotations` accept an inline JSON value or `-` to read JSON from stdin, capped at 1 MiB.

## Exit codes

```text
0   success
1   execution failure
2   run cancelled / cancel succeeded
3   `up` ended in waiting-approval, waiting-event, or waiting-timer
4   invalid arguments / user-correctable input error
130 SIGINT
143 SIGTERM
```

## Output envelope and next-step CTAs

On a TTY the CLI prints human-formatted text. When output is piped or captured (any non-TTY consumer, such as an AI agent), each command instead emits a single TOON envelope: the command's `data`, plus a "Next steps" `cta` block that suggests follow-up commands. The `cta` renders as a TOON table:

```toon
commands[2]{command,description}:
  bunx smithers-orchestrator logs run-abc123,Tail active run
  bunx smithers-orchestrator inspect run-abc123,Inspect most recent run
```

Read each row as two comma-separated fields declared by the `{command,description}` header. **The runnable shell command is the first field, up to the comma; the text after the comma is a human description, not part of the command.** So the first row's command is `bunx smithers-orchestrator logs run-abc123`, and `Tail active run` is prose. Never copy-paste a whole row into a shell. Field values are unquoted unless they themselves contain a comma or a quote, in which case TOON quotes and escapes them (a `--prompt "..."` value comes out as `"... --prompt \"<describe>\""`); strip that TOON quoting before running. On a TTY the same CTA renders as aligned `command  # description` lines, so this parsing only matters for piped or agent output. Pass `--format json` to get the same `cta` as a nested object with explicit `command` and `description` keys and skip parsing the table.

## Pauses, resume, and detached runs

`bunx smithers-orchestrator up` exits when a run reaches a durable wait state (`waiting-approval`, `waiting-event`, or `waiting-timer`), even in foreground mode. `bunx smithers-orchestrator up --detach` starts a background owner and returns its `runId`, but that owner also exits when the run pauses. This is expected: the persisted run is waiting for an external decision, signal, or timer rather than burning a process.

To drive a run to completion across pauses, use the Gateway (`getRun`, `streamRunEventsResilient`, `submitApproval`, `submitSignal`, and `resumeRun`) for an automated keeper. For ad-hoc operation, the equivalent public CLI commands are `ps`, `why RUN_ID`, `inspect RUN_ID`, `logs RUN_ID -f`, `approve`/`deny`, `signal`, and `up <workflow.tsx> --resume RUN_ID`. Use `--force` only after confirming the previous owner is gone or intentionally replacing it. Never implement the keeper by querying the run store.

If resume fails with `RESUME_METADATA_MISMATCH`, the workflow file changed after the run started. Resume validates the original workflow metadata and source hash; it is not a hot-reload mechanism for stopped runs. Start a fresh run instead, for example with a new `--run-id`, or overwrite an existing planned id with `--force` when that command supports it. When iterating on a workflow definition, expect each edit to require a fresh run rather than `--resume`.

## Interactive mode and the full-screen TUI monitor

`bunx smithers-orchestrator up --interactive` (or just `bunx smithers-orchestrator up` / `bunx smithers-orchestrator workflow run` from an interactive TTY without a positional argument) opens a terminal UI that guides you through three steps:

1. **Workflow picker** - fuzzy-search your installed workflows and select one.
2. **Input prompts** - fill required fields for the chosen workflow.
3. **Full-screen monitor** - the run starts in a detached background process and the terminal switches into a full-screen view that tracks it live.

The monitor connects to a local Gateway on port 7331, starting one automatically if none is running.  It exits when you press `q`.

**Agents: hand humans interactive commands.** When you (an AI agent) give a human a command to run themselves, include the `--interactive` flag whenever the command supports it (`bunx smithers-orchestrator up --interactive`, `bunx smithers-orchestrator workflow run WORKFLOW_ID --interactive`), so the human lands in this full-screen monitor instead of a detached log tail. Reserve the non-interactive forms for CI, scripts, and the commands you execute yourself with your shell tool. Never pass `--interactive` to a command you run programmatically, since it opens a full-screen TUI your harness cannot drive.

### Monitor modes and keybindings

The monitor opens in **Tree** mode. Switch modes with the letter aliases below. From any *non-Tree* mode the number keys `1`–`5` also jump straight to a mode (and `1` returns to Tree).

| Mode | Reach it with | What you see |
|------|---------------|--------------|
| **Tree** | `1` (from another mode) | Node tree with per-node output, logs, diffs, and props; inline approval banner when waiting |
| **Graph** | `g` (toggles Tree ↔ Graph), or `2` | Directed dependency graph; `j`/`k` navigate, `⏎` inspects a node in Tree |
| **Logs** | `l`, or `3` | Filtered event stream (up to 2 000 events); `[`/`]` filter by attempt, `f` toggles follow |
| **Timeline** | `t`, or `4` | Horizontal tick strip you scrub with `j`/`k` to inspect node state at past frames; `L` jumps back to live (inspect-only; rewind with `bunx smithers-orchestrator rewind`) |
| **Hijack** | `h`, or `5` | Hand off to `bunx smithers-orchestrator hijack` for an active node |
| **Quit** | `q` (or Ctrl-C) | (quit) |

Inside **Tree** mode the number keys are **inspector tabs, not mode switches**: `1` output, `2` logs, `3` diff, `4` props (`←`/`→` also walk the tabs). Use the `g`/`l`/`t`/`h` aliases to leave Tree.

The status header shrinks to a compact one-liner on terminals narrower than 100 columns; the keybar adapts the same way.

## Monitoring a background run

A detached run (`up --detach` / `run --detach`) and an MCP `run_workflow` background launch both execute where the user has no live view of progress. To close that gap, the CLI returns a `monitoring` block with the run and prints agent-directed guidance in the "Next steps" CTA: offer the user one of these ways to watch the run, then set up whichever they pick.

1. **Smithers Monitor (live, zero setup):** run `bunx smithers-orchestrator monitor RUN_ID` to open the gateway's live web UI over every run in the workspace, focused on this one (status, execution tree, events, approvals), no code required. See the Smithers Monitor.
2. **Status-report cron (hands-off):** a Bun job that every 5 minutes calls Gateway `getRun` and reports the status; use `streamRunEventsResilient` while the process is awake.
3. **Live custom UI (richest, most work):** run `bunx smithers-orchestrator ui RUN_ID` when the workflow declares a `.smithers/ui/<workflow>.tsx` UI with `<UI entry="../ui/<workflow>.tsx" />`, otherwise author the UI source and declaration first. See custom workflow UIs.
4. **Quick HTML page (fastest):** write a static status page from Gateway `getRun` / `getDevToolsSnapshot`, open it, and refresh it about every 5 minutes.

The same `monitoring` object (its `text` plus structured `options`) is in the `run_workflow` MCP result for background launches, and is `null` when `waitForTerminal` is set (the run already finished).

## Workflow UIs

Open a run's custom browser UI with:

```sh
bunx smithers-orchestrator ui RUN_ID
```

`bunx smithers-orchestrator ui` discovers the workspace singleton, verifies that the answering process serves this workspace, and uses its advertised URL. Discovery refuses gateways that advertise another workspace; use `--gateway <url>` or `--no-autostart` when you need to be explicit. When none is running it starts `smithers gateway` automatically (one per workspace: concurrent autostarts race on a lock and share the winner) and then opens the UI. `smithers gateway` serves workspace run state and workflow-owned `<UI>` declarations, with entry files resolved relative to the workflow that declared them. A workspace with no local `.smithers` pack and no prior run store still boots and serves the global pack alone. That is the headless sandbox shape: provision a VM with `bunx smithers-orchestrator init --global --yes --no-skill` (the default `bun install` inside `~/.smithers` is required for pack workflows to import) and `smithers gateway` serves the full pack, UIs included, from any bare cloned repo. Manage the daemon with `smithers gateway status` and `smithers gateway stop`. Use `bunx smithers-orchestrator ui RUN_ID --no-autostart` to fail fast when no Gateway is already running, or `--gateway <url>` for a remote Gateway.

## Troubleshooting

- `RESUME_METADATA_MISMATCH`: the workflow source or metadata changed since the run began. Start a fresh run instead of resuming across the edit.
- Bundled jj exits with `EACCES`: the optional `@smithers-orchestrator/jj-<platform>` binary is present but not executable. Run `chmod +x node_modules/@smithers-orchestrator/jj-*/bin/jj`, reinstall Smithers, or set `SMITHERS_JJ_PATH` to a working `jj` binary. Confirm resolution with `bunx smithers-orchestrator workflow doctor`.

## Command catalog (TOON)

Commands listed by dotted name. `human` and `alerts` use an action positional instead of nested subcommands.

```toon
commands[89]:
  - name: init
    purpose: Install the local Smithers workflow pack into .smithers/. In an interactive terminal init asks one question (your preferred coding agent), installs the pack with defaults plus that agent's plugin (or skill if no plugin), then opens a hijacked tutorial session hosted by that agent; piped/agent/CI runs (or --yes) install defaults. Pass an optional prompt to also launch the create-workflow builder after init.
    args[1]{name,type,required,desc}:
      prompt,string,false,Optional plain-English task: after init, launch the create-workflow builder with this prompt pre-filled
    flags[12]{name,short,type,default,desc}:
      agent,,string,,Preferred coding agent id (e.g. claude, codex, pi); skips the interactive agent question
      tutorial,,boolean,true,After install, open a hijacked tutorial session hosted by your preferred agent (interactive init only); --no-tutorial skips
      force,,boolean,false,Overwrite existing scaffold files
      agents-only,,boolean,false,Only create .smithers/agents/ and leave workflow pack untouched
      install,,boolean,true,Run bun install inside the pack after scaffolding
      add-agents,,boolean,false,Launch the account registration wizard after scaffolding
      skill,,boolean,true,Install the curated smithers skill into detected coding agents and append workflow guidance to existing CLAUDE.md/AGENTS.md files
      global,,boolean,false,Scaffold the global pack in ~/.smithers (honors SMITHERS_HOME) instead of ./.smithers
      update-prompt,,boolean,true,In an interactive terminal, ask which drifted shipped pack files to update (warns on shared components); --no-update-prompt skips
      template,,string,,Show next steps for a canonical starter template ID after init
      yes,,boolean,false,Non-interactive: skip prompts and use defaults; alias for --non-interactive
      non-interactive,,boolean,false,Non-interactive: skip prompts and use defaults; alias for --yes
  - name: make-workflow
    purpose: Build a new Smithers workflow from a plain-English description. Dispatches to the create-workflow builder.
    args[1]{name,type,required,desc}:
      task,string,false,Plain-English description of the workflow to build (forwarded as the builder prompt)
    flags[35]{name,short,type,default,desc}:
      detach,d,boolean,false,Background mode; print runId/pid/logFile and exit
      run-id,r,string,,Explicit run ID
      max-concurrency,c,number,4,Maximum parallel tasks
      root,,string,,Tool sandbox root directory
      log,,boolean,true,Enable NDJSON event log file output
      log-dir,,string,,NDJSON event logs directory
      allow-network,,boolean,false,Allow bash tool network requests
      max-output-bytes,,number,,Max bytes a single tool call can return
      tool-timeout-ms,,number,,Max wall-clock time per tool call in ms
      hot,,boolean,false,Hot reload for .tsx workflows
      input,i,string,,Input data as JSON string or '-' to read JSON from stdin
      annotations,,string,,Run annotations as flat JSON string/number/boolean object or '-' to read JSON from stdin
      resume,,boolean|string,false,Resume an existing run; may be true or a run ID
      force,,boolean,false,Resume even if still marked running
      resume-claim-owner,,string,,Internal durable resume claim owner
      resume-claim-heartbeat,,number,,Internal durable resume claim heartbeat
      resume-restore-owner,,string,,Internal durable resume restore owner
      resume-restore-heartbeat,,number,,Internal durable resume restore heartbeat
      serve,,boolean,false,Start an HTTP server alongside the workflow
      supervise,,boolean,false,Run stale-run supervisor loop with --serve
      supervise-dry-run,,boolean,false,With --supervise; detect without resuming
      supervise-interval,,string,10s,Supervisor poll interval
      supervise-stale-threshold,,string,30s,Heartbeat staleness threshold
      supervise-max-concurrent,,number,3,Max runs resumed per poll
      port,,number,7331,HTTP server port when --serve
      host,,string,127.0.0.1,HTTP bind address when --serve
      auth-token,,string,,Bearer token for HTTP auth or SMITHERS_API_KEY env
      insecure,,boolean,false,Allow unauthenticated non-loopback HTTP binding; dangerous
      metrics,,boolean,true,Expose /metrics endpoint when --serve
      backend,,enum,,Bootstrap storage selection for a workflow owner or workspace Gateway; not a run-discovery/control flag (sqlite|pglite|postgres)
      post-failure,,boolean,true,Auto-launch the post-failure autopsy workflow when this run fails (disable with --no-post-failure or SMITHERS_POST_FAILURE=0)
      verbose,,boolean,false,Show engine info logs (run lifecycle, agent sessions) on interactive runs
      report,,boolean,true,Narrate the result with a cheap agent and open an HTML summary on interactive runs (disable with --no-report or SMITHERS_NO_REPORT=1)
      prompt,p,string,,Prompt text mapped to input.prompt when --input is omitted
      interactive,,boolean,false,Pick inputs through interactive terminal prompts and live-render the run
  - name: starters
    purpose: Show plain-English starter workflows with copy-paste commands
    args[1]{name,type,required,desc}:
      id,string,false,Starter ID or alias
    flags[4]{name,short,type,default,desc}:
      audience,,string,,Filter by audience such as product, support, or founder
      goal,,string,,Filter by goal such as plan, build, debug, or quality
      workflow,,string,,Filter by seeded workflow ID
      tag,,string,,Filter by starter tag
  - name: hermes
    purpose: Add Smithers to a local Hermes agent (register the MCP server and install the native Hermes plugin); alias for mcp add --agent hermes
  - name: up
    purpose: Start or resume a workflow execution from a discovered workflow ID or a .tsx workflow file path
    args[1]{name,type,required,desc}:
      workflow,string,false,Workflow ID or file path (omit with --interactive to pick one)
    flags[34]{name,short,type,default,desc}:
      detach,d,boolean,false,Background mode; print runId/pid/logFile and exit
      run-id,r,string,,Explicit run ID
      max-concurrency,c,number,4,Maximum parallel tasks
      root,,string,,Tool sandbox root directory
      log,,boolean,true,Enable NDJSON event log file output
      log-dir,,string,,NDJSON event logs directory
      allow-network,,boolean,false,Allow bash tool network requests
      max-output-bytes,,number,,Max bytes a single tool call can return
      tool-timeout-ms,,number,,Max wall-clock time per tool call in ms
      hot,,boolean,false,Hot reload for .tsx workflows
      input,i,string,,Input data as JSON string or '-' to read JSON from stdin
      annotations,,string,,Run annotations as flat JSON string/number/boolean object or '-' to read JSON from stdin
      resume,,boolean|string,false,Resume an existing run; may be true or a run ID
      force,,boolean,false,Resume even if still marked running
      resume-claim-owner,,string,,Internal durable resume claim owner
      resume-claim-heartbeat,,number,,Internal durable resume claim heartbeat
      resume-restore-owner,,string,,Internal durable resume restore owner
      resume-restore-heartbeat,,number,,Internal durable resume restore heartbeat
      serve,,boolean,false,Start an HTTP server alongside the workflow
      supervise,,boolean,false,Run stale-run supervisor loop with --serve
      supervise-dry-run,,boolean,false,With --supervise; detect without resuming
      supervise-interval,,string,10s,Supervisor poll interval
      supervise-stale-threshold,,string,30s,Heartbeat staleness threshold
      supervise-max-concurrent,,number,3,Max runs resumed per poll
      port,,number,7331,HTTP server port when --serve
      host,,string,127.0.0.1,HTTP bind address when --serve
      auth-token,,string,,Bearer token for HTTP auth or SMITHERS_API_KEY env
      insecure,,boolean,false,Allow binding a non-loopback --host with NO auth (exposes unauthenticated approve/deny/cancel control of the run; dangerous)
      metrics,,boolean,true,Expose /metrics Prometheus endpoint when --serve
      interactive,,boolean,false,Pick a workflow and inputs interactively then open the full-screen run monitor (no silent fallback: fails with TUI_MONITOR_UNAVAILABLE if the tui package is missing, leaving the detached run running)
      backend,,enum,,Bootstrap storage selection for a workflow owner or workspace Gateway; not a run-discovery/control flag (sqlite|pglite|postgres)
      post-failure,,boolean,true,Auto-launch the post-failure autopsy workflow when this run fails (disable with --no-post-failure or SMITHERS_POST_FAILURE=0)
      verbose,,boolean,false,Show engine info logs (run lifecycle, agent sessions) on interactive runs
      report,,boolean,true,Narrate the result with a cheap agent and open an HTML summary on interactive runs (disable with --no-report or SMITHERS_NO_REPORT=1)
  - name: migrate
    purpose: Copy the legacy bun:sqlite smithers.db into PGlite or Postgres and write the migrated.json marker
    flags[3]{name,short,type,default,desc}:
      to,,enum,pglite,Target backend (pglite|postgres)
      url,,string,,Postgres connection URL when --to postgres
      keep-sqlite,,boolean,true,Keep the legacy SQLite database after a successful copy
  - name: eval
    purpose: Run a workflow over JSON/JSONL cases and write a regression report
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path or discovered workflow ID
    flags[16]{name,short,type,default,desc}:
      cases,c,string,,JSON array, { cases: [...] }, or JSONL case file
      suite,s,string,,Stable suite ID used in run IDs and report paths
      run-label,,string,current UTC timestamp + nonce,Label appended to eval run IDs
      dry-run,n,boolean,false,Plan run IDs without launching workflows
      concurrency,j,number,1,Number of eval cases to run at once
      max-cases,,number,,Run only the first N cases
      report,r,string,.smithers/evals/<suite>.json,Report path
      force,,boolean,false,Overwrite an existing report
      include-output,,boolean,true,Include workflow outputs in the report
      max-concurrency,,number,,Per-workflow task concurrency
      root,,string,,Tool sandbox root directory
      log,,boolean,true,Enable NDJSON event log file output
      log-dir,,string,,NDJSON event logs directory
      allow-network,,boolean,false,Allow bash tool network requests
      max-output-bytes,,number,,Max bytes a single tool call can return
      tool-timeout-ms,,number,,Max wall-clock time per tool call in ms
      optimization,,string,,Apply a Smithers optimization artifact while running the eval suite
  - name: optimize
    purpose: Run GEPA prompt optimization over a workflow eval suite and write an optimized prompt artifact
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path or discovered workflow ID
    flags[16]{name,short,type,default,desc}:
      cases,c,string,,JSON array, { cases: [...] }, or JSONL case file
      suite,s,string,,Stable suite ID used in run IDs and report paths
      provider,p,enum,openai-api,GEPA patch generator provider
      model,m,string,,Optimizer model for provider-backed GEPA
      artifact,a,string,,Write the optimized prompt artifact to this path
      report-dir,,string,,Directory for baseline and optimized eval reports
      min-improvement,,number,0.000001,Minimum required absolute score improvement
      max-cases,,number,,Run only the first N cases
      concurrency,j,number,1,Number of eval cases to run at once
      max-concurrency,,number,,Per-workflow task concurrency
      root,,string,,Tool sandbox root directory
      log,,boolean,true,Enable NDJSON event log file output
      log-dir,,string,,NDJSON event logs directory
      allow-network,,boolean,false,Allow bash tool network requests
      max-output-bytes,,number,,Max bytes a single tool call can return
      tool-timeout-ms,,number,,Max wall-clock time per tool call in ms
  - name: supervise
    purpose: Watch for stale running runs and auto-resume them
    flags[4]{name,short,type,default,desc}:
      dry-run,n,boolean,false,Detect stale runs without resuming
      interval,i,string,10s,Poll interval
      stale-threshold,t,string,30s,Heartbeat staleness threshold before resume
      max-concurrent,c,number,3,Max runs resumed per poll
  - name: gateway
    purpose: Serve the multi-run Gateway RPC/WS control plane for workspace run state (one singleton per workspace; a second start refuses); unlike up --serve, this is not tied to one run
    args[1]{name,type,required,desc}:
      action,enum,false,Manage the running singleton instead of serving: status | stop
    flags[7]{name,short,type,default,desc}:
      host,H,string,127.0.0.1,Gateway bind address
      port,p,number,7331,Preferred port (falls back to an ephemeral port when taken; clients discover the verified URL with gateway status)
      backend,,enum,,Storage behind this workspace Gateway; a boot/deployment choice, not a client run-lookup flag (sqlite|pglite|postgres)
      auth-token,,string,,Bearer token for HTTP/WS auth (or SMITHERS_API_KEY); required for a non-loopback host
      mint-token,,boolean,false,Mint a random bearer; require it on every request and record it only in the 0600 runtime state file
      insecure,,boolean,false,Allow a non-loopback host with NO auth (dangerous)
      idle-timeout,,number,,Exit after this many ms with no clients, in-flight runs, or registered schedules (0 = stay up; autostarted daemons set this automatically); overridable via SMITHERS_GATEWAY_IDLE_MS
  - name: monitor
    purpose: Open the Smithers Monitor, the gateway's live web UI over every run in the workspace (served at /monitor, no .smithers pack required). Pure observation; launches no run, no workflow, no agents. Resolves the workspace's singleton gateway (--gateway probe, then runtime-state discovery, then legacy port probe, then autostart) and opens the browser
    args[1]{name,type,required,desc}:
      runId,string,false,Focus this run when the monitor opens (deep-linked as ?runId=)
    flags[5]{name,short,type,default,desc}:
      gateway,g,string,,Gateway base URL (default http://127.0.0.1:<port>)
      port,,number,7331,Gateway port when --gateway is not set
      open,,boolean,true,Open a browser; use --no-open to just print the URL
      autostart,,boolean,true,Start a local Gateway automatically when no Gateway is reachable; --no-autostart fails fast instead
      daemon,,boolean,true,Autostart the Gateway as a background daemon; --no-daemon (or SMITHERS_NO_DAEMON=1) disables daemonized autostart
  - name: bug
    purpose: File a smithers bug report to bug.smithers.sh; with --run it attaches the run's workflow, status, error, and last ~50 events with secrets scrubbed
    flags[4]{name,short,type,default,desc}:
      run,,string,,Attach this run's workflow name, status, error, and recent events to the report
      title,,string,,Bug title (derived from the run's error when omitted)
      body,,string,,Bug description body
      endpoint,,string,https://bug.smithers.sh/api/bugs,Bug endpoint URL; the SMITHERS_BUG_ENDPOINT env var takes precedence
  - name: review
    purpose: Run code review plus story-form HTML walkthrough generation for a repo or PR
    args[1]{name,type,required,desc}:
      repo,string,false,Repository path; defaults to the current directory
    flags[17]{name,short,type,default,desc}:
      from,,string,,Base ref for a merge-base diff
      to,,string,,Head ref for a merge-base diff
      commit,,string,,Review one commit
      pr,,string,,Review a GitHub PR and post the review onto it
      background,,string,,Requirement background for review and narration
      no-review,,boolean,false,Skip review agents
      no-narrate,,boolean,false,Skip the narrator agent
      no-verify,,boolean,false,Skip verification over findings
      quiz,,enum,auto,off|auto|on
      concurrency,,number,8,Parallel file reviews
      timeout,,number,10,Per-agent-task timeout in minutes
      out,,string,,Output HTML path
      title,,string,,Walkthrough title (default: narrator headline)
      db,,string,,Smithers db path
      split,,boolean,false,Side-by-side diffs instead of unified
      publish,,boolean,false,Upload to the share service and print the share URL
      open,,boolean,false,Open the walkthrough in the default browser
  - name: ps
    purpose: List active, paused, and recently completed runs
    flags[5]{name,short,type,default,desc}:
      status,s,string,,"Filter: running|waiting-approval|waiting-event|waiting-timer|paused|continued|finished|failed|cancelled"
      limit,l,number,20,Max rows
      all,a,boolean,false,Include all statuses
      watch,w,boolean,false,Refresh continuously
      interval,i,number,2,Watch refresh seconds
  - name: logs
    purpose: Tail lifecycle events for a run
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[5]{name,short,type,default,desc}:
      follow,f,boolean,true,Poll for new events while run is active
      from-seq,,number,,Start from event sequence number (exclusive)
      since,,number,,Deprecated alias of --from-seq (an event sequence number, not a duration)
      tail,n,number,50,Last N events
      follow-ancestry,,boolean,false,Include ancestor run events root-to-current
  - name: events
    purpose: Query run event history with filters, grouping, and NDJSON output
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[8]{name,short,type,default,desc}:
      node,n,string,,Filter by node ID
      type,t,string,,"Category: agent|approval|frame|memory|node|openapi|output|revert|run|sandbox|scorer|snapshot|supervisor|timer|token|tool-call|workflow"
      since,s,string,,Recent duration window such as 5m or 2h
      limit,l,number,1000,Max events; capped at 100000
      json,j,boolean,false,Emit NDJSON
      group-by,,string,,"node | attempt"
      watch,w,boolean,false,Append new events as they arrive
      interval,i,number,2,Watch poll seconds
  - name: chat
    purpose: Show agent chat output for the latest run or a specific run
    args[1]{name,type,required,desc}:
      runId,string,false,Run ID; latest run if omitted
    flags[4]{name,short,type,default,desc}:
      all,a,boolean,false,Show every agent attempt
      follow,f,boolean,false,Watch for new output
      tail,n,number,,Last N chat blocks
      stderr,,boolean,true,Include agent stderr
  - name: chat-create
    purpose: Create and start a one-task auto-hijacked chat run
    flags[2]{name,short,type,default,desc}:
      agent,,enum,,claude-code|codex|antigravity|pi|kimi|amp
      cwd,,string,.,Working directory for the chat session
  - name: hijack
    purpose: Hand off the latest resumable agent session or Smithers-managed conversation
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[3]{name,short,type,default,desc}:
      target,,string,,"Expected engine such as claude-code or codex"
      timeout-ms,,number,30000,Wait time for live handoff
      launch,,boolean,true,Open session immediately
  - name: inspect
    purpose: Output detailed state of a run: steps, agents, approvals, timers, loops, outputs
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[2]{name,short,type,default,desc}:
      watch,w,boolean,false,Refresh continuously
      interval,i,number,2,Watch refresh seconds
  - name: node
    purpose: Show enriched node details for debugging retries, tool calls, and output
    args[1]{name,type,required,desc}:
      nodeId,string,true,Node ID
    flags[6]{name,short,type,default,desc}:
      run-id,r,string,,Run ID containing the node
      iteration,i,number,,Loop iteration; latest if omitted
      attempts,,boolean,false,Expand all attempts in human output
      tools,,boolean,false,Expand tool input/output payloads
      watch,w,boolean,false,Refresh continuously
      interval,,number,2,Watch refresh seconds
  - name: why
    purpose: Explain why a run is currently blocked or paused
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[1]{name,short,type,default,desc}:
      json,,boolean,false,Structured JSON diagnosis
  - name: what
    purpose: Summarize what happened in a run or node with a cheap fast agent (deterministic recap without one)
    args[1]{name,type,required,desc}:
      runId,string,false,Run ID (default: latest run)
    flags[4]{name,short,type,default,desc}:
      node,n,string,,Node ID: explain one node instead of the whole run
      iteration,i,number,,Loop iteration number (default: latest)
      json,,boolean,false,Structured JSON (summary, agentId, source, facts)
      timeout,,number,,Narrator agent timeout in seconds (default 60)
  - name: human
    purpose: List, answer, or cancel durable human requests
    args[2]{name,type,required,desc}:
      action,string,true,inbox|answer|cancel
      requestId,string,false,Human request ID for answer/cancel
    flags[2]{name,short,type,default,desc}:
      value,,string,,JSON response for answer
      by,,string,,Human operator identifier
  - name: ask-human
    purpose: Raise a blocking human-approval request from inside a run and wait for the decision
    args[1]{name,type,required,desc}:
      prompt,string,true,The decision or question to put to a human
    flags[7]{name,short,type,default,desc}:
      context,,string,,Extra context appended to the prompt
      choices,,string,,Comma-separated choices for a fixed-choice decision
      run-id,r,string,,Run to attach to (SMITHERS_RUN_ID or single active run)
      node,n,string,,Node id to attach to (SMITHERS_NODE_ID)
      iteration,,number,0,Loop iteration (SMITHERS_ITERATION or 0)
      timeout,,number,,Seconds before the request expires
      poll,,number,3,Poll interval in seconds while blocking
  - name: alerts
    purpose: List and manage durable alert instances
    args[2]{name,type,required,desc}:
      action,string,true,list|ack|resolve|silence
      alertId,string,false,Alert ID for ack/resolve/silence
  - name: approve
    purpose: Approve a paused approval gate; auto-detects the node if only one is pending
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[4]{name,short,type,default,desc}:
      node,n,string,,Node ID required if multiple approvals are pending
      iteration,,number,0,Loop iteration
      note,,string,,Approval note
      by,,string,,Approver identifier
  - name: deny
    purpose: Deny a paused approval gate
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[4]{name,short,type,default,desc}:
      node,n,string,,Node ID required if multiple approvals are pending
      iteration,,number,0,Loop iteration
      note,,string,,Denial note
      by,,string,,Denier identifier
  - name: signal
    purpose: Deliver a durable signal to a run waiting on Signal or WaitForEvent
    args[2]{name,type,required,desc}:
      runId,string,true,Run ID
      signalName,string,true,Signal name
    flags[3]{name,short,type,default,desc}:
      data,,string,,Signal payload as JSON; defaults to {}
      correlation,,string,,Correlation ID to match a specific waiter
      by,,string,,Signal sender identifier
  - name: cancel
    purpose: Safely halt agents and terminate one active run
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
  - name: pause
    purpose: Gracefully pause a run; let in-flight tasks finish, then park it resumably
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
  - name: down
    purpose: Cancel all active runs in the current Smithers workspace
    flags[1]{name,short,type,default,desc}:
      force,,boolean,false,Cancel runs even if they still appear live; without this only stale runs are cancelled
  - name: graph
    purpose: Render the workflow graph without executing it
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow ID or file path
    flags[3]{name,short,type,default,desc}:
      run-id,r,string,graph,Run ID for context
      input,,string,,Input JSON; overrides persisted input
      root,,string,,Tool sandbox root directory (same anchor as up)
  - name: gui
    purpose: Open a directory as a workspace in Smithers UI
    args[1]{name,type,required,desc}:
      path,string,false,Directory path (defaults to current working directory)
    flags[5]{name,short,type,default,desc}:
      gateway,g,string,,Gateway base URL (default http://127.0.0.1:<port>)
      port,,number,7331,Gateway port when --gateway is not set
      workflow,w,string,,Open this workflow's UI directly skipping run lookup
      open,,boolean,true,Open a browser; use --no-open to just print the URL
      autostart,,boolean,true,Start a local Gateway automatically when no Gateway is reachable
  - name: ui
    purpose: Open the custom UI for a workflow run in your browser
    args[1]{name,type,required,desc}:
      runId,string,false,Run to open. Defaults to the most recent run.
    flags[4]{name,short,type,default,desc}:
      gateway,g,string,,Gateway base URL (default http://127.0.0.1:<port>)
      port,,number,7331,Gateway port when --gateway is not set
      workflow,w,string,,Open this workflow's UI directly skipping run lookup
      open,,boolean,true,Open a browser; use --no-open to just print the URL
  - name: revert
    purpose: Revert the workspace to a previous task attempt's filesystem state
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path
    flags[4]{name,short,type,default,desc}:
      run-id,r,string,,Run ID
      node-id,n,string,,Node ID
      attempt,,number,1,Attempt number
      iteration,,number,0,Loop iteration
  - name: retry-task
    purpose: Retry a specific task within a run, then resume the workflow
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path
    flags[5]{name,short,type,default,desc}:
      run-id,r,string,,Run ID
      node-id,n,string,,Task/node ID to retry
      iteration,,number,0,Loop iteration
      no-deps,,boolean,false,Only reset this node; skip dependents
      force,,boolean,false,Allow retry even if run is still running
  - name: timetravel
    purpose: Time-travel to a task state; revert filesystem, reset DB, optionally resume
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path
    flags[8]{name,short,type,default,desc}:
      run-id,r,string,,Run ID
      node-id,n,string,,Task/node ID
      iteration,,number,0,Loop iteration
      attempt,a,number,,Attempt number; latest if omitted
      no-vcs,,boolean,false,Skip filesystem revert; DB only
      no-deps,,boolean,false,Only reset this node
      resume,,boolean,false,Resume after time travel
      force,,boolean,false,Force even if run is still running
  - name: replay
    purpose: Fork from a checkpoint and resume execution
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path
    flags[6]{name,short,type,default,desc}:
      run-id,r,string,,Source run ID
      frame,f,number,,Frame number to fork from
      node,n,string,,Node ID to reset to pending
      input,i,string,,Input overrides as JSON
      label,l,string,,Branch label for the fork
      restore-vcs,,boolean,false,Restore jj filesystem state to source frame revision
  - name: fork
    purpose: Create a branched run from a snapshot checkpoint
    args[1]{name,type,required,desc}:
      workflow,string,true,Workflow file path
    flags[6]{name,short,type,default,desc}:
      run-id,r,string,,Source run ID
      frame,f,number,,Frame number to fork from
      reset-node,n,string,,Node ID to reset to pending
      input,i,string,,Input overrides as JSON
      label,l,string,,Branch label
      run,,boolean,false,Immediately start the forked run
  - name: timeline
    purpose: View execution timeline for a run and its forks
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[2]{name,short,type,default,desc}:
      tree,,boolean,false,Include all child forks recursively
      json,j,boolean,false,Output as JSON
  - name: tree
    purpose: Print DevTools snapshot as an XML tree
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[6]{name,short,type,default,desc}:
      frame,,number,,Historical frame number
      watch,,boolean,false,Stream live DevTools events
      json,j,boolean,false,Emit snapshot JSON
      depth,,number,,Truncate depth
      node,,string,,Scope to subtree
      color,,enum,auto,auto|always|never
  - name: diff
    purpose: Print a node DiffBundle as unified diff
    args[2]{name,type,required,desc}:
      runId,string,true,Run ID containing the node
      nodeId,string,true,Node ID to diff
    flags[4]{name,short,type,default,desc}:
      iteration,,number,,Loop iteration; latest if omitted
      json,j,boolean,false,Emit raw DiffBundle
      stat,,boolean,false,Show stat summary only
      color,,enum,auto,auto|always|never
  - name: output
    purpose: Print a node output row
    args[2]{name,type,required,desc}:
      runId,string,true,Run ID containing the node
      nodeId,string,true,Node ID to fetch output for
    flags[3]{name,short,type,default,desc}:
      iteration,,number,,Loop iteration; latest if omitted
      json,j,boolean,true,Emit raw row as JSON
      pretty,,boolean,false,Schema-ordered render
  - name: rewind
    purpose: Rewind a run to a previous frame
    args[2]{name,type,required,desc}:
      runId,string,true,Run ID to rewind
      frameNo,number,true,Target frame number
    flags[2]{name,short,type,default,desc}:
      yes,,boolean,false,Skip confirmation prompt
      json,j,boolean,false,Emit JumpResult JSON
  - name: snapshots
    purpose: List durability snapshots (workspace checkpoints) for a run
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID to list snapshots for
    flags[1]{name,short,type,default,desc}:
      json,j,boolean,false,Emit rows as JSON
  - name: restore
    purpose: Restore a worktree to a durability checkpoint (latest for the node, or --seq)
    args[2]{name,type,required,desc}:
      runId,string,true,Run ID containing the checkpoint
      nodeId,string,true,Node ID whose worktree to restore
    flags[2]{name,short,type,default,desc}:
      iteration,,number,,Loop iteration
      seq,,number,,Checkpoint seq; latest if omitted
  - name: snapshot-hook
    purpose: "Internal: PostToolUse hook that requests a Tier 1 durability snapshot"
  - name: observability
    purpose: Start or stop the local Docker Compose observability stack
    flags[2]{name,short,type,default,desc}:
      detach,d,boolean,false,Run containers in the background
      down,,boolean,false,Stop and remove the stack
  - name: ask
    purpose: Ask a question about Smithers using an installed agent and the Smithers MCP server
    args[1]{name,type,required,desc}:
      question,string,false,Question to ask
    flags[6]{name,short,type,default,desc}:
      agent,,enum,,claude|codex|antigravity|kimi|pi
      list-agents,,boolean,false,List detected agents and exit
      dump-prompt,,boolean,false,Print generated system prompt and exit
      tool-surface,,enum,semantic,semantic|raw
      no-mcp,,boolean,false,Disable MCP bootstrap and use prompt fallback
      print-bootstrap,,boolean,false,Print selected bootstrap configuration and exit
  - name: scores
    purpose: View scorer results for a specific run
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID
    flags[1]{name,short,type,default,desc}:
      node,,string,,Filter scores to a specific node ID
  - name: usage
    purpose: Show how much rate limit or subscription quota each registered account has used
    flags[3]{name,short,type,default,desc}:
      account,,string,,Only report this account label
      provider,,string,,Only report accounts for this provider
      fresh,,boolean,false,Bypass the short usage cache while respecting provider rate-limit floors
  - name: docs
    purpose: Print llms.txt for this CLI version
    flags[2]{name,short,type,default,desc}:
      latest,,boolean,false,Fetch the latest docs from smithers.sh instead of docs for this CLI version
      docs-version,,string,,Fetch docs for a specific Smithers version, e.g. 0.22.0 or v0.22.0
  - name: docs-full
    purpose: Print llms-full.txt for this CLI version
    flags[2]{name,short,type,default,desc}:
      latest,,boolean,false,Fetch the latest docs from smithers.sh instead of docs for this CLI version
      docs-version,,string,,Fetch docs for a specific Smithers version, e.g. 0.22.0 or v0.22.0
  - name: update
    purpose: Check for a newer Smithers release and upgrade the install (or print how)
    flags[2]{name,short,type,default,desc}:
      check,,boolean,false,Only report current vs latest version; never upgrade
      dry-run,,boolean,false,Print the upgrade command without running it
  - name: upgrade
    purpose: "Run the agent-assisted Smithers upgrade workflow: fetch changelogs, upgrade with a cheap agent, and escalate to a smart agent only when needed."
    flags[8]{name,short,type,default,desc}:
      interactive,,boolean,false,Force the full-screen interactive TUI monitor (TTY only).
      detach,d,boolean,false,Launch the upgrade workflow in the background and print the run ID.
      dry-run,,boolean,false,Fetch changelogs and plan the upgrade without changing the install.
      run-id,,string,,Explicit run ID for the upgrade workflow.
      root,,string,,Tool sandbox root directory.
      log-dir,,string,,NDJSON event logs directory.
      backend,,enum,,"sqlite|pglite|postgres"
      auth-token,,string,,Bearer token passed to the interactive monitor gateway client.
  - name: agents.capabilities
    purpose: Print JSON capability registry for built-in CLI agents
  - name: agents.doctor
    purpose: Validate built-in CLI agent capability registries and command-surface contracts
    flags[1]{name,short,type,default,desc}:
      json,,boolean,false,Print doctor report as JSON
  - name: agents.add
    purpose: Register a Smithers agent account, interactively or with flags
    flags[9]{name,short,type,default,desc}:
      provider,,enum,,"claude-code|antigravity|codex|kimi|anthropic-api|openai-api|gemini-api"
      label,,string,,Unique account label
      config-dir,,string,,Per-account CLI config dir for subscription providers
      api-key,,string,,API key for API-key providers
      model,,string,,Default model for this account
      skip-login,,boolean,false,Skip credential-directory check
      force,,boolean,false,Register even if no credentials are present
      replace,,boolean,false,Overwrite an existing account with the same label
      loop,,boolean,false,Wizard mode; keep adding accounts until done
  - name: agents.list
    purpose: List registered Smithers agent accounts
  - name: agents.remove
    purpose: Remove a registered agent account by label
    args[1]{name,type,required,desc}:
      label,string,true,Account label
    flags[1]{name,short,type,default,desc}:
      silent,,boolean,false,Do not error if the label is not registered
  - name: agents.test
    purpose: Spawn an account's underlying CLI with --version
    args[1]{name,type,required,desc}:
      label,string,true,Account label
  - name: workflow.list
    purpose: List discovered workflows (local .smithers/workflows/ plus the global ~/.smithers/workflows/; each entry reports its scope, local shadows global)
  - name: workflow.run
    purpose: Run a discovered workflow by ID
    args[1]{name,type,required,desc}:
      name,string,false,Workflow ID (omit with --interactive to pick one)
    flags[35]{name,short,type,default,desc}:
      detach,d,boolean,false,Background mode; print runId/pid/logFile and exit
      run-id,r,string,,Explicit run ID
      max-concurrency,c,number,4,Maximum parallel tasks
      root,,string,,Tool sandbox root directory
      log,,boolean,true,Enable NDJSON event log file output
      log-dir,,string,,NDJSON event logs directory
      allow-network,,boolean,false,Allow bash tool network requests
      max-output-bytes,,number,,Max bytes a single tool call can return
      tool-timeout-ms,,number,,Max wall-clock time per tool call in ms
      hot,,boolean,false,Hot reload for .tsx workflows
      input,i,string,,Input data as JSON string or '-' to read JSON from stdin
      annotations,,string,,Run annotations as flat JSON string/number/boolean object or '-' to read JSON from stdin
      resume,,boolean|string,false,Resume an existing run; may be true or a run ID
      force,,boolean,false,Resume even if still marked running
      resume-claim-owner,,string,,Internal durable resume claim owner
      resume-claim-heartbeat,,number,,Internal durable resume claim heartbeat
      resume-restore-owner,,string,,Internal durable resume restore owner
      resume-restore-heartbeat,,number,,Internal durable resume restore heartbeat
      serve,,boolean,false,Start an HTTP server alongside the workflow
      supervise,,boolean,false,Run stale-run supervisor loop with --serve
      supervise-dry-run,,boolean,false,With --supervise; detect without resuming
      supervise-interval,,string,10s,Supervisor poll interval
      supervise-stale-threshold,,string,30s,Heartbeat staleness threshold
      supervise-max-concurrent,,number,3,Max runs resumed per poll
      port,,number,7331,HTTP server port when --serve
      host,,string,127.0.0.1,HTTP bind address when --serve
      auth-token,,string,,Bearer token for HTTP auth or SMITHERS_API_KEY env
      insecure,,boolean,false,Allow binding a non-loopback --host with NO auth (exposes unauthenticated approve/deny/cancel control of the run; dangerous)
      metrics,,boolean,true,Expose /metrics Prometheus endpoint when --serve
      backend,,enum,,Bootstrap storage selection for a workflow owner or workspace Gateway; not a run-discovery/control flag (sqlite|pglite|postgres)
      post-failure,,boolean,true,Auto-launch the post-failure autopsy workflow when this run fails (disable with --no-post-failure or SMITHERS_POST_FAILURE=0)
      verbose,,boolean,false,Show engine info logs on interactive runs; the default keeps progress + warnings only (non-TTY/structured output always gets full logs)
      report,,boolean,true,Narrate an interactive run's result with a cheap/fast agent and open an HTML summary in the browser (disable with --no-report or SMITHERS_NO_REPORT=1)
      prompt,p,string,,Shorthand for input.prompt when --input is omitted
      interactive,,boolean,false,Pick a workflow and inputs interactively then open the full-screen run monitor (no silent fallback: fails with TUI_MONITOR_UNAVAILABLE if the tui package is missing, leaving the detached run running)
  - name: workflow.path
    purpose: Resolve a workflow ID to its entry file path
    args[1]{name,type,required,desc}:
      name,string,true,Workflow ID
  - name: workflow.inspect
    purpose: Show workflow metadata and an agent-facing skill preview
    args[1]{name,type,required,desc}:
      name,string,true,Workflow ID
  - name: workflow.create
    purpose: Create a new flat workflow scaffold in .smithers/workflows/ (or ~/.smithers with --global)
    args[1]{name,type,required,desc}:
      name,string,true,New workflow ID
    flags[1]{name,short,type,default,desc}:
      global,,boolean,false,Create in the global ~/.smithers pack (honors SMITHERS_HOME) instead of the local .smithers
  - name: workflow.skills
    purpose: Generate agent-facing skill docs for discovered workflows
    args[1]{name,type,required,desc}:
      name,string,false,Workflow ID; omit for all workflows
    flags[3]{name,short,type,default,desc}:
      output,,string,,Output file for one workflow, or output directory for all
      force,,boolean,false,Overwrite existing skill files
      global,,boolean,false,Write skills into the global ~/.smithers pack instead of the local .smithers
  - name: workflow.doctor
    purpose: Inspect workflow discovery, preload files, bunfig, and detected agents
    args[1]{name,type,required,desc}:
      name,string,false,Workflow ID; omit for all
  - name: claude.tick
    purpose: One /workflows mirror frame for a run (Claude Code plugin protocol, contract v1); --wait blocks until a mirror-relevant event lands after --after-seq
    args[1]{name,type,required,desc}:
      runId,string,true,Run ID to mirror
    flags[6]{name,short,type,default,desc}:
      after-seq,,number,0,Event-log cursor from the previous tick's seq
      wait,,boolean,false,Block until a mirror-relevant event lands after --after-seq (or timeout)
      timeout-ms,,number,420000,Max wait in ms before returning timedOut true
      interval-ms,,number,750,Wait poll interval in ms
      max-output-chars,,number,2000,Truncate node outputs to this many chars
      collapse-phases,,boolean,false,Collapse the phase plan to a single phase
  - name: claude.node-wait
    purpose: Block until one node reaches a terminal state, then print its final state and output (returns timedOut true on expiry; re-invoke to keep waiting)
    args[1]{name,type,required,desc}:
      nodeId,string,true,Node ID to wait on
    flags[5]{name,short,type,default,desc}:
      run-id,,string,,Run ID that owns the node
      iteration,i,number,,Loop iteration (default latest)
      timeout-ms,,number,480000,Max wait in ms before returning timedOut true
      interval-ms,,number,1000,Poll interval in ms
      max-output-chars,,number,2000,Truncate the node output to this many chars
  - name: claude.monitor
    purpose: Follow local runs and print one NDJSON line per actionable transition (approval pending, human request, failed, stalled); --transitions all adds finished/cancelled/continued; backs the plugin's background monitor
    flags[4]{name,short,type,default,desc}:
      interval-ms,,number,2000,Poll interval in ms
      stalled-after-ms,,number,120000,Heartbeat age that flags a running run as stalled
      ticks,,number,,Stop after N polls (default run until killed)
      transitions,,string,actionable,Which transitions stream: actionable or all
  - name: cron.start
    purpose: Start the background scheduler loop in the current terminal
  - name: cron.add
    purpose: Register a new workflow cron schedule
    args[2]{name,type,required,desc}:
      pattern,string,true,Cron expression
      workflowPath,string,true,Path or ID of workflow to schedule
  - name: cron.list
    purpose: List registered background cron schedules
  - name: cron.rm
    purpose: Delete a cron schedule by ID
    args[1]{name,type,required,desc}:
      cronId,string,true,Cron ID
  - name: memory.list
    purpose: List all memory facts in a namespace
    args[1]{name,type,required,desc}:
      namespace,string,true,Namespace such as workflow:my-flow
    flags[1]{name,short,type,default,desc}:
      workflow,w,string,,Path to workflow file that locates the DB
  - name: memory.get
    purpose: Get a single memory fact by namespace and key
    args[2]{name,type,required,desc}:
      namespace,string,true,Namespace such as workflow:my-flow
      key,string,true,Fact key
    flags[1]{name,short,type,default,desc}:
      workflow,w,string,,Path to workflow file that locates the DB
  - name: memory.set
    purpose: Set a memory fact (value stored verbatim as the fact's JSON value)
    args[3]{name,type,required,desc}:
      namespace,string,true,Namespace such as workflow:my-flow
      key,string,true,Fact key
      value,string,true,Fact value stored as-is
    flags[2]{name,short,type,default,desc}:
      workflow,w,string,,Path to workflow file that locates the DB
      ttl,,number,,Time-to-live in milliseconds
  - name: memory.rm
    purpose: Delete a memory fact by namespace and key
    args[2]{name,type,required,desc}:
      namespace,string,true,Namespace such as workflow:my-flow
      key,string,true,Fact key
    flags[1]{name,short,type,default,desc}:
      workflow,w,string,,Path to workflow file that locates the DB
  - name: openapi.list
    purpose: Preview tools generated from an OpenAPI spec
    args[1]{name,type,required,desc}:
      specPath,string,true,File path or URL to OpenAPI spec
  - name: openapi.generate
    purpose: Generate an AI SDK tools module from an OpenAPI spec
    args[2]{name,type,required,desc}:
      specPath,string,true,File path to OpenAPI spec
      outputPath,string,true,Output JavaScript file for generated tools
  - name: token.issue
    purpose: Issue a local short-lived Gateway bearer token grant
    flags[6]{name,short,type,default,desc}:
      scopes,,string,run:read,Comma or space separated Gateway scopes
      role,,string,operator,Role recorded on the token grant
      user-id,,string,,User ID recorded on the token grant
      ttl,,string,1h,Token lifetime such as 15m or 1h
      action-id,,string,gateway,Action id allowed to resolve the brokered action token
      reveal-token,,boolean,false,Include the raw bearer token in CLI output
  - name: token.exec
    purpose: Resolve an action token locally and inject the bearer into a child process environment
    flags[5]{name,short,type,default,desc}:
      handle,,string,,Brokered action token handle
      action-id,,string,gateway,Action id expected by the brokered token
      scopes,,string,,Comma or space separated scopes required for this action
      env,,string,SMITHERS_API_KEY,Environment variable that receives the bearer token
      command,,string,,Shell command to run with the injected token
  - name: token.revoke
    purpose: Revoke a locally issued Gateway bearer token
    args[1]{name,type,required,desc}:
      token,string,true,Bearer token to revoke
  - name: completions
    purpose: Generate shell completion scripts
    args[1]{name,type,required,desc}:
      shell,string,true,bash|fish|nushell|zsh
  - name: mcp.add
    purpose: Register Smithers as an MCP server for an agent integration
    flags[3]{name,short,type,default,desc}:
      agent,,string,,Target agent such as claude-code or cursor
      command,c,string,,Override the command agents will run
      no-global,,boolean,false,Install to project instead of globally
  - name: skills.add
    purpose: Sync skill files to agent integrations
    flags[2]{name,short,type,default,desc}:
      depth,,number,1,Grouping depth for skill files
      no-global,,boolean,false,Install to project instead of globally
  - name: skills.list
    purpose: List available skills
```

## Operational notes

- **Detached mode** (`up --detach`): redirects stdout/stderr to a log file, prints `runId`/`pid`/`logFile`, and exits.
- **Serve mode** (`up --serve`): starts the HTTP app and keeps the process alive until interrupted. Add `--supervise` to run stale-run recovery in the same process.
- **Watch mode**: `ps`, `events`, `inspect`, `node`, and `tree` have watch-style behavior. They stop cleanly on SIGINT and most stop when the run becomes terminal.
- **DevTools commands**: `tree`, `diff`, `output`, and `rewind` intentionally use command-scoped `--json`/`-j` and return exit code `1` for parser/user errors.
- **Account commands**: `agents add|list|remove|test` manage `~/.smithers/accounts.json`; subscription providers use CLI config directories, API providers use API keys. Legacy entries with an unknown provider are preserved across `add`/`remove`; `agents remove <label>` deletes one.
- **Output format**: all commands honour `--format toon|json|yaml|md|jsonl`; `--filter-output <key.path>` extracts a nested field from JSON output.
- **Interactive TUI**: use `bunx smithers-orchestrator up --interactive` or `bunx smithers-orchestrator workflow run WORKFLOW_ID --interactive` for the current full-screen terminal monitor. `bunx smithers-orchestrator init` also has a human setup flow (one agent question, then a hijacked tutorial). See Interactive TUI.

---

## CLI Quickstart

> Short operational cheatsheet. The full catalog lives in CLI.

**Prerequisites:** `bun` installed, and a project directory where you want to run Smithers.

<Note>
  This cheatsheet is for the agent operating Smithers. You, the agent, run every
  command on this page for the human; never hand them to the human. The human
  states outcomes and answers questions; you operate the CLI.
</Note>

**Set up**

```bash
bunx smithers-orchestrator init                                       # scaffold .smithers/
bunx smithers-orchestrator init --template idea-to-tickets            # scaffold with guided template next steps
bunx smithers-orchestrator starters                                   # browse template IDs
```

**Verify the setup**

```bash
bunx smithers-orchestrator --version          # prints the installed version
bunx smithers-orchestrator workflow doctor    # vcs + workflow pack health
bunx smithers-orchestrator workflow list      # workflows discovered in this repo
```

`workflow doctor` confirms a VCS resolved and the pack scaffolded (output trimmed):

```text
vcs:
  jj:
    path: jj
    source: path
  git:
    path: git
    source: path
  ok: true
workflowRoot: /your/repo/.smithers
packs[2]:
  - scope: local
    packDir: /your/repo/.smithers
    preload:
      path: /your/repo/.smithers/preload.ts
      exists: true
    bunfig:
      path: /your/repo/.smithers/bunfig.toml
      exists: true
  - scope: global
    ...
workflows[46]:
  - id: audit
  - id: implement
  - ...
```

`ok: true` under `vcs` means jj or git resolved; if it is `false`, install one (see Installation). A non-empty `workflows[...]` count means the pack is in place. `workflow list` prints the same workflow entries with full metadata.

**Run**

```bash
bunx smithers-orchestrator workflow run implement --prompt "…"        # launch a seeded workflow
bunx smithers-orchestrator up workflow.tsx --run-id RUN_ID --resume true   # resume a paused run
```

**What success looks like**

`workflow run` and `up` stream the lifecycle event log and exit on a terminal line:

```text
[12:00:01] ▶ Run started
[12:00:01] ↺ Run status: running
[12:00:38] ✓ implement (attempt 1)
[12:00:40] ✓ Run finished
```

`✓ Run finished` means the run completed. `⏸ <node> waiting for approval` means it paused for a human decision (relay the question to the human, then clear it with `approve` and resume). `✗ Run failed: …` reports the error. The CLI prints the run id and suggested next commands (`logs`, `inspect`) after launch; pass that id to every command below.

**Observe & control**

```bash
bunx smithers-orchestrator ps                                         # list runs
bunx smithers-orchestrator inspect RUN_ID                           # structured run state
bunx smithers-orchestrator logs RUN_ID --tail 20 --follow           # stream events
bunx smithers-orchestrator why RUN_ID                               # why is it paused?
bunx smithers-orchestrator approve RUN_ID --node NODE_ID --by NAME   # approve a paused node
bunx smithers-orchestrator cancel RUN_ID                            # cancel a run
```

See Tour for a full worked example. See CLI overview for the complete flag reference.

---

## <Workflow>

> Root container; sequences direct children, optionally caches for resume.

```ts
import { Workflow } from "smithers-orchestrator";

type WorkflowProps = {
  name: string;
  cache?: boolean; // skip already-completed nodes on resume
  children?: ReactNode;
};
```

```tsx
import { CodexAgent, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, Task, smithers, outputs } = createSmithers({
  research: z.object({ findings: z.string() }),
});

const researcher = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  instructions: "You are a research assistant.",
});

export default smithers((ctx) => (
  <Workflow name="research-pipeline" cache>
    <Task id="research" output={outputs.research} agent={researcher}>
      {`Research: ${ctx.input.topic}`}
    </Task>
  </Workflow>
));
```

## Notes

- Direct children run sequentially; `<Sequence>` is only needed inside other control-flow components.
- **Two ways to get `<Workflow>`, both equivalent.** You can either `import { Workflow } from "smithers-orchestrator"` (top-level export) or destructure it from `createSmithers(...)`; they render the identical node. The factory's `Workflow` is a thin pass-through to the same component (it injects no schema or context), unlike `Task`/`Approval`/`Sandbox`/`Signal`, which the factory wires to the run's context and your output schemas. **Convention: destructure `Workflow` from `createSmithers` alongside `Task`, `smithers`, and `outputs`** so the workflow's components all come from one place; this is what the example above and ~95% of `examples/` do. Importing `Workflow` (or `Sequence`, `Branch`, `Loop`, `Parallel`, also pure structural components) directly from `"smithers-orchestrator"` is equally valid and handy in tiny single-file workflows.
- Custom Drizzle tables require `runId`, `nodeId`, and `iteration` columns with a composite primary key `(run_id, node_id, iteration)`. Tasks outside a `<Loop>` write `iteration = 0`.

---

## <Task>

> Executable node; runs an agent, compute callback, or emits a static value.

```ts
import { Task } from "smithers-orchestrator";

type TaskProps = {
  id: string;
  output: z.ZodObject | Table | string;
  outputSchema?: z.ZodObject; // inferred when output is a Zod schema
  agent?: AgentLike | AgentLike[]; // array = [primary, ...fallbacks]
  fallbackAgent?: AgentLike;
  dependsOn?: string[];
  needs?: Record<string, string>;
  deps?: Record<string, OutputTarget>; // typed render-time upstream outputs
  depsOptional?: boolean; // render even when some deps are unresolved (skip them)
  fork?: string; // start from another task's final agent session snapshot
  allowTools?: string[]; // CLI-agent tool allowlist
  key?: string;
  skipIf?: boolean;
  needsApproval?: boolean; // pause for human before executing
  async?: boolean; // with needsApproval: let unrelated flow continue
  timeoutMs?: number;
  retries?: number; // default Infinity with exponential backoff
  noRetry?: boolean;
  retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number };
  continueOnFail?: boolean;
  cache?: { by?: (ctx) => unknown; version?: string; key?: string; ttlMs?: number; scope?: "run" | "workflow" | "global" };
  label?: string;
  meta?: Record<string, unknown>;
  scorers?: ScorersMap;
  memory?: {
    recall?: { namespace?: string; query?: string; topK?: number };
    remember?: { namespace?: string; key?: string };
    threadId?: string;
  };
  heartbeatTimeoutMs?: number; // fail if no heartbeat in window
  heartbeatTimeout?: number; // alias of heartbeatTimeoutMs
  hijack?: boolean; // request an immediate hijack handoff as soon as the task starts running
  onHijackExit?: "complete" | "reopen"; // what Smithers should do after a hijacked session exits
  children?:
    | string
    | Row
    | (() => Row | Promise<Row>)
    | ReactNode
    | ((deps) => Row | ReactNode);
};
```

`output={outputs.someKey}` is the canonical schema path. `outputs` comes from
the same `createSmithers(...)` call as the workflow, and each value is the exact
Zod schema object you registered. When a Task renders, Smithers uses that object
as the output target, infers `outputSchema`, passes it to native
structured-output agents, validates the returned row, and persists it to the
matching output table. You only need `outputSchema={...}` when `output` is a
custom Drizzle table or string key and Smithers cannot infer the Zod schema.

<Warning>
**Do not name an output field `iteration`, `runId`, or `nodeId`.** Smithers persists every node output alongside those internal columns, so an output schema that declares one collides and the workflow fails to load (`bunx smithers-orchestrator graph` errors before it runs). This bites loop counters especially; name the field `round`, `pass`, or `attempt` instead of `iteration`.
</Warning>

```tsx
import { CodexAgent } from "smithers-orchestrator";

const analysisAgent = new CodexAgent({
  model: "gpt-5.6-terra",
  instructions: "You are a careful software analyst.",
});

const reviewAgent = new CodexAgent({
  model: "gpt-5.6-sol",
  instructions: "You are a senior code reviewer.",
});

<Task id="analyze" output={outputs.analysis} agent={analysisAgent}>
  {`Analyze: ${ctx.input.repoPath}`}
</Task>

<Task id="review" output={outputs.review} agent={reviewAgent} deps={{ analyze: outputs.analysis }}>
  {(deps) => `Review: ${deps.analyze.summary}`}
</Task>
```

## Three task modes: agent, compute, static

The `children` shape decides what a Task does. No `agent` prop is needed for the
compute and static modes.

- **Agent** (`agent={...}`, children is a prompt string or `(deps) => string`):
  runs the agent, parses its output against the schema, persists.
- **Compute** (no `agent`, children is a function returning a value): runs your
  code. The function **may be `async`** and is awaited, so do real work here
  (run a test command, call an API, read a file). Whatever it returns is
  validated against `output` and persisted, exactly like an agent's parsed JSON.
- **Static** (no `agent`, children is a literal value): persists the value as-is.

```tsx
// Compute task: an async function body runs real work, no agent involved.
<Task id="run-tests" output={outputs.testResult}>
  {async () => {
    const proc = Bun.spawn(["bun", "test"], { stdout: "pipe", stderr: "pipe" });
    const stdout = await new Response(proc.stdout).text();
    const code = await proc.exited;
    return { passed: code === 0, log: stdout.slice(-4000) };
  }}
</Task>

// Compute task reading an upstream output via deps; still just a function.
<Task id="count" output={outputs.count} deps={{ analyze: outputs.analysis }}>
  {async (deps) => ({ issues: deps.analyze.issues.length })}
</Task>

// Static task: a literal value, persisted as-is.
<Task id="config" output={outputs.config}>
  {{ region: "us-east-1", retries: 3 }}
</Task>
```

A compute or static Task is a first-class node: it persists, resumes, gates
downstream `deps`, and works inside `<Loop>`, `<Branch>`, `<Parallel>`, and
`<Sequence>` just like an agent Task. The only thing it cannot do is be a `fork`
source (no agent session). Throwing inside a compute function fails the task and
triggers its retry policy.

## Dependencies: dependsOn, needs, deps

Three props express upstream dependencies. They compose.

- **`dependsOn: string[]`**: ordering only. The task waits until every listed task id is terminal. No values are passed in.
- **`needs: Record<string, string>`**: named dependencies. Each value is an upstream task id; each key is the name that value is looked up under.
- **`deps: Record<string, OutputTarget>`**: typed render-time outputs. The task gates on each dependency and passes the resolved rows to a `children` callback via `{(deps) => ...}`.
- **`depsOptional: boolean`**: tolerate unresolved deps. By default a task with `deps` defers until every dependency has an output row. With `depsOptional`, the task renders as soon as it can, and any dependency that has no row (for example an upstream `continueOnFail` task that failed) is simply absent from the `deps` object. Read those values defensively: `deps.summary?.text`, because an unresolved dep is missing, not present-but-undefined.

The detail that bites people: **a `deps` key is treated as the upstream task's id.** `deps={{ analyze: outputs.analysis }}` depends on a task whose id is `analyze`. If your dep name differs from the upstream id, remap it with `needs`:

```tsx
// Upstream task id is "parse-summary", but you want to call it `summary` locally.
<Task id="parse-summary" output={outputs.summary}>{/* ... */}</Task>

<Task id="report" output={outputs.report}
  needs={{ summary: "parse-summary" }}   // map the key to the real task id
  deps={{ summary: outputs.summary }}>
  {(deps) => deps.summary.text}
</Task>
```

Without the `needs` remap, `deps={{ summary: ... }}` depends on a node id `summary` that no task produces. The dependency can never resolve, and the run fails with `DEPENDENCY_DEADLOCK` naming the stuck task. (Previously this hung or finished while silently skipping the task.) `needs` alone works too when you don't need the typed `children` callback.

### Across a loop boundary

A task inside a `<Loop>` can depend on a task outside the loop. The upstream is resolved at its own iteration, not the loop's, so `deps`/`needs` reach it from any iteration:

```tsx
<Task id="config" output={outputs.config}>{/* runs once, outside the loop */}</Task>

<Loop id="work" until={done}>
  <Task id="step" output={outputs.step}
    needs={{ config: "config" }}
    deps={{ config: outputs.config }}>
    {(deps) => useRegion(deps.config.region)}
  </Task>
</Loop>
```

## Fork

Every agent task produces a reusable session snapshot. Use `fork` to start a new task from any previous task's context.

`<Task id={B} fork={A}>` means:

- `B` depends on `A` and cannot run until `A` has completed.
- `B` starts from a **copy** of `A`'s final agent session context, then submits its own prompt into that copy.
- `B` produces its own output and its own session snapshot. `A` is never mutated.

`fork` is immutable. It does not continue or mutate the source task; it copies the conversation into a fresh, independent session. Multiple tasks may fork the same source safely, and a forked task may itself be forked.

```tsx
import { CodexAgent } from "smithers-orchestrator";

const planner = new CodexAgent({ model: "gpt-5.6-sol" });
const implementer = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });

const PLAN = "plan" as const;
const IMPLEMENT = "implement" as const;
const VERIFY = "verify" as const;

<Task id={PLAN} agent={planner} output={outputs.plan}>
  Make a plan.
</Task>

<Task id={IMPLEMENT} agent={implementer} fork={PLAN} output={outputs.patch}>
  Implement the plan.
</Task>

<Task id={VERIFY} agent={implementer} fork={IMPLEMENT} output={outputs.result}>
  Run tests and fix failures.
</Task>
```

`VERIFY` forks `IMPLEMENT`, which forked `PLAN`, so `VERIFY` sees the whole `plan → implement` conversation.

**Parallel branches**: fork the same source from sibling tasks; each gets its own copy and they never affect each other:

```tsx
import { CodexAgent } from "smithers-orchestrator";

const researcher = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });
const implementer = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });

<Task id="investigate" agent={researcher} output={outputs.investigation}>
  Understand the bug and identify possible fixes.
</Task>

<Parallel>
  <Task id="minimal-fix" agent={implementer} fork="investigate" output={outputs.patch}>
    Try the minimal fix.
  </Task>
  <Task id="refactor-fix" agent={implementer} fork="investigate" output={outputs.patch}>
    Try the refactor fix.
  </Task>
</Parallel>
```

`fork` composes with `dependsOn`, `needs`, `deps`, `Sequence`, `Parallel`, `Branch`, and `Loop`. Inside a loop, `fork` resolves to the **latest completed** session snapshot for that task id; there is no iteration selector and no ambiguity.

### Error cases

- **`TASK_FORK_SOURCE_NOT_FOUND`**: `fork` points to a task id not present in the graph (including a source that exists only in an unselected `<Branch>`).
- **`TASK_FORK_CYCLE`**: `fork` creates a cycle, directly or indirectly.
- **`TASK_FORK_SESSION_UNAVAILABLE`**: the forking task is not an agent task, or the source completed but produced no usable session snapshot (e.g. a compute/static source, or a source that was skipped/cancelled).
- **`TASK_FORK_SOURCE_NOT_COMPLETE`**: the source exists but has not completed; the forked task waits and does not run.

## Notes

- Three modes by `children` shape: agent (with `agent`), compute (function, no agent), static (value, no agent).
- `fork` requires an agent task; the source must be an agent task with a session snapshot. Forking copies the conversation into a new session and never reuses a native session id.
- When `outputSchema` is set, JSON is extracted from agent text; schema-validation retries don't consume `retries`.
- Auth errors short-circuit retries; non-idempotent tool reuse warns on the next attempt.

---

## <Sequence>

> Run children in source order.

```ts
import { Sequence } from "smithers-orchestrator";

type SequenceProps = {
  key?: string;
  skipIf?: boolean;
  children?: ReactNode;
};
```

```tsx
<Workflow name="build-and-deploy">
  <Parallel maxConcurrency={2}>
    <Sequence>
      <Task id="build-frontend" output={outputs.buildFrontend}>
        {{ status: "built" }}
      </Task>
      <Task id="test-frontend" output={outputs.testFrontend}>
        {{ passed: true }}
      </Task>
    </Sequence>
    <Sequence>
      <Task id="build-backend" output={outputs.buildBackend}>
        {{ status: "built" }}
      </Task>
      <Task id="test-backend" output={outputs.testBackend}>
        {{ passed: true }}
      </Task>
    </Sequence>
  </Parallel>
</Workflow>
```

## Notes

- `<Workflow>` sequences direct children implicitly; `<Sequence>` is only needed inside other control-flow components.
- Empty `<Sequence>` is valid and produces no tasks.

---

## <Parallel>

> Run children concurrently with an optional concurrency cap.

```ts
import { Parallel } from "smithers-orchestrator";

type ParallelProps = {
  id?: string;
  label?: string; // display name for this group in run views (graph, /workflows mirror)
  maxConcurrency?: number; // default: no per-group cap; effective concurrency is min(this, the run-level --max-concurrency, which defaults to 4)
  subtreeConcurrency?: number; // opt-in: cap DIRECT CHILDREN (whole subtrees) in flight, not leaf tasks
  skipIf?: boolean;
  children?: ReactNode;
};
```

```tsx
<Workflow name="checks">
  <Parallel maxConcurrency={2}>
    <Task id="lint" output={outputs.lint}>
      {{ errors: 0 }}
    </Task>
    <Task id="typecheck" output={outputs.typecheck}>
      {{ passed: true }}
    </Task>
    <Task id="test" output={outputs.test}>
      {{ passed: true }}
    </Task>
  </Parallel>
</Workflow>
```

## Two concurrency semantics

`<Parallel>` supports two independent caps that may coexist:

- **`maxConcurrency` (leaf-task cap).** Caps the tasks whose *innermost*
  enclosing group is this parallel. A nested `<Parallel>` starts its own group,
  so its tasks do **not** count against the outer cap. Use it when the children
  are flat tasks (like the checks example above).
- **`subtreeConcurrency` (subtree cap, opt-in, int >= 1).** Caps how many
  **direct children** of this parallel are *in flight* at once, where every
  descendant task counts toward the child it descends from. A child is in
  flight from its first started task until all of its tasks finish; when a
  child finishes, the freed slot admits the next child in document order.
  An in-flight child may always finish its remaining tasks (even the ones in
  nested parallels) while over-cap siblings wait.

The difference matters as soon as children are subtrees. In a kanban-shaped
workflow, tickets fanned out in parallel with each ticket a sequence of
implement then reviews, `maxConcurrency={4}` neither means "4 tickets" (each ticket's
first task belongs to the outer group, but the nested review parallel escapes
it) nor bounds reviews. `subtreeConcurrency={4}` means exactly "4 tickets in
flight":

```tsx
<Workflow name="kanban">
  <Parallel id="tickets" subtreeConcurrency={2}>
    {tickets.map((ticket) => (
      <Sequence key={ticket.id} label={ticket.title}>
        <Task id={`${ticket.id}-implement`} agent={coder} output={outputs.impl}>
          Implement {ticket.title}
        </Task>
        {/* Nested parallel: 3 reviewers per ticket. Still counts toward the
            ticket's in-flight slot; its own cap bounds simultaneous reviews. */}
        <Parallel id={`${ticket.id}-reviews`} maxConcurrency={3}>
          {reviewers.map((reviewer) => (
            <Task
              key={reviewer.id}
              id={`${ticket.id}-review-${reviewer.id}`}
              agent={reviewer.agent}
              output={outputs.review}
            >
              Review the implementation of {ticket.title}
            </Task>
          ))}
        </Parallel>
      </Sequence>
    ))}
  </Parallel>
</Workflow>
```

Here at most 2 tickets are worked at once; within an active ticket at most 3
reviews run concurrently; the run-level cap still bounds total in-flight tasks.

### Subtree cap notes

- Descendant tasks record their **nearest** ancestor `<Parallel subtreeConcurrency>`:
  nesting a second subtree-capped parallel inside a child re-scopes its
  descendants to the inner group.
- Child identity is the direct child's explicit `key`/`id` prop when the
  element carries one through (e.g. `<Task id>`, `<Parallel id>`,
  `<Worktree id>`), otherwise its child ordinal. Keep direct-child order stable
  (or give ids) so resumed runs re-admit the same children.
- Admission is deterministic in document order and derived purely from task
  states, so a resumed run makes the same decisions from a restored state map.

## Notes

- Group completes when all children finish. To let the group proceed past a failing child, set `continueOnFail` on that individual child `<Task>`. It is not a `<Parallel>` prop.
- Children receive `parallelGroupId` and `parallelMaxConcurrency` in their descriptor; under a subtree cap every descendant task also receives `subtreeGroupId`, `subtreeChildKey`, and `subtreeMax`.
- A run-level cap (`bunx smithers-orchestrator up --max-concurrency, -c`, default `4`) bounds the total in-flight tasks across the whole run, applied on top of every group. So a group can never exceed it: `<Parallel>` with no cap runs at most 4 children at once, and even `<Parallel maxConcurrency={5}>` is silently truncated to 4. To fan out wider, raise `-c` (or set the run's `maxConcurrency`) as well. `subtreeConcurrency` composes the same way: leaf-group cap, subtree cap, and the run-level cap must all pass.

---

## <Branch>

> Conditional fork; mounts `then` when `if` is true, otherwise `else`.

```ts
import { Branch } from "smithers-orchestrator";

type BranchProps = {
  if: boolean;
  then: ReactElement;
  else?: ReactElement | null;
  skipIf?: boolean;
};
```

```tsx
import { createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, Task, Branch, smithers, outputs } = createSmithers({
  input: z.object({
    deployToProduction: z.boolean(),
  }),
  deployment: z.object({
    environment: z.enum(["staging", "production"]),
    url: z.string(),
  }),
});

export default smithers((ctx) => (
  <Workflow name="deploy-pipeline">
    <Branch
      if={ctx.input.deployToProduction}
      then={
        <Task id="deploy-production" output={outputs.deployment}>
          {{ environment: "production", url: "https://prod.example.com" }}
        </Task>
      }
      else={
        <Task id="deploy-staging" output={outputs.deployment}>
          {{ environment: "staging", url: "https://staging.example.com" }}
        </Task>
      }
    />
  </Workflow>
));
```

Use a plain boolean for `if`. The example above reads immutable run input, so
there is no stale-read hazard: `ctx.input.deployToProduction` is the same value
on every frame of that run. When the condition depends on an upstream task, read
with `ctx.outputMaybe(...)`, coalesce the missing first-frame value, and let the
render loop re-evaluate after the upstream task persists:

```tsx
const test = ctx.outputMaybe(outputs.test, { nodeId: "test" });

<Branch
  if={test?.passed === true}
  then={<Task id="deploy" output={outputs.deployment}>...</Task>}
  else={<Task id="notify" output={outputs.notification}>...</Task>}
/>
```

## Notes

- `if` re-evaluates every render frame; run input is immutable, and task outputs become visible on later frames after they persist.
- Read incomplete upstream outputs with `ctx.outputMaybe()` rather than `ctx.output()`.
- Each branch takes one element; wrap multiples in `<Sequence>` or `<Parallel>`.
- Unselected branch tasks are absent from the task graph.
- `<Branch>` takes **no children**; pass branches via `then`/`else`. Writing `<Branch>...</Branch>` throws `INVALID_INPUT` (children would otherwise be silently dropped).

---

## <Loop>

> Re-run children until `until` is true or `maxIterations` is hit.

```ts
import { Loop } from "smithers-orchestrator";

type LoopProps = {
  key?: string;
  id?: string; // give an explicit id; auto-generated from tree position otherwise
  until?: boolean;
  maxIterations?: number; // default 5
  onMaxReached?: "fail" | "return-last"; // default "return-last"
  continueAsNewEvery?: number; // checkpoint every N iters to bound history
  skipIf?: boolean;
  children?: ReactNode;
};
```

```tsx
export default smithers((ctx) => {
  const latestReview = ctx.latest("review", "review");

  return (
    <Workflow name="refine-loop">
      <Loop until={latestReview?.approved === true} maxIterations={5}>
        <Sequence>
          <Task id="write" output={outputs.draft} agent={writer}>
            {latestReview
              ? `Improve the draft. Feedback: ${latestReview.feedback}`
              : `Write a draft about: ${ctx.input.topic}`}
          </Task>
          <Task id="review" output={outputs.review} agent={reviewer}>
            {`Review the latest draft.`}
          </Task>
        </Sequence>
      </Loop>
    </Workflow>
  );
});
```

Loop children are ordinary Tasks, so a loop can drive **compute** tasks (an
`async` function body, no agent) just as well as agents. A retry-until-green
test loop is the canonical non-agent case:

```tsx
export default smithers((ctx) => {
  const last = ctx.latest("testRun", "run-tests");

  return (
    <Workflow name="retry-tests">
      <Loop id="until-green" until={last?.passed === true} maxIterations={3} onMaxReached="fail">
        <Task id="run-tests" output={outputs.testRun}>
          {async () => {
            const proc = Bun.spawn(["bun", "test"], { stdout: "pipe" });
            const log = await new Response(proc.stdout).text();
            return { passed: (await proc.exited) === 0, log };
          }}
        </Task>
      </Loop>
    </Workflow>
  );
});
```

## Notes

- Give every `<Loop>` an explicit `id`. Without one the loop id is derived from its position in the tree, so a sibling that conditionally mounts or unmounts shifts the path and re-keys the loop, stranding its iteration state and restarting it from 0. An explicit `id` pins identity and makes the loop immune to sibling churn.
- `onMaxReached` has exactly two values. `"return-last"` (the default) ends the loop cleanly once `maxIterations` is hit, keeping the last iteration's output. `"fail"` instead throws `RALPH_MAX_REACHED` and fails the run. Use `"fail"` when hitting the cap means the work never converged (tests never went green); use the default when the best-so-far result is acceptable.
- `ctx.latest(table, nodeId)` reads the highest-iteration output; `until` must use `ctx.outputMaybe()` since output is absent on iter 0.
- Direct nesting of `<Loop>` in `<Loop>` throws; wrap the inner loop in `<Sequence>`.
- Custom Drizzle tables for loop tasks require `iteration` in the primary key.
- `Ralph` is still exported as a deprecated alias of `Loop` for older workflows. New code should import and render `Loop`.

---

## <Approval>

> Durable human approval; persists ApprovalDecision, selection, or ranking.

```ts
import { Approval, approvalDecisionSchema } from "smithers-orchestrator";

type ApprovalProps = {
  id: string;
  mode?: "approve" | "select" | "rank"; // default "approve"
  options?: ApprovalOption[]; // required for select/rank
  output: z.ZodObject | Table | string;
  outputSchema?: z.ZodObject; // default: mode-appropriate schema (approve→approvalDecisionSchema, select→approvalSelectionSchema, rank→approvalRankingSchema); or the output schema itself when output is a ZodObject
  request: { title: string; summary?: string; metadata?: Record<string, unknown> };
  onDeny?: "fail" | "continue" | "skip"; // default "fail"
  allowedScopes?: string[];
  allowedUsers?: string[];
  autoApprove?: {
    after?: number; // auto-approve after N consecutive manual approvals
    condition?: (ctx: WorkflowContext) => boolean;
    audit?: boolean;
    revertOn?: (ctx: WorkflowContext) => boolean;
  };
  async?: boolean; // unrelated downstream may continue while pending
  dependsOn?: string[];
  needs?: Record<string, string>;
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;
  retries?: number;
  retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number };
  continueOnFail?: boolean;
  cache?: { by?: (ctx) => unknown; version?: string };
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: React.ReactNode;
};
```

```tsx
import { Approval, Sequence, Task, Workflow, approvalDecisionSchema, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  publishApproval: approvalDecisionSchema,
  publishResult: z.object({ status: z.enum(["published", "rejected"]) }),
});

export default smithers((ctx) => {
  const decision = ctx.outputMaybe(outputs.publishApproval, { nodeId: "approve-publish" });
  return (
    <Workflow name="publish-flow">
      <Sequence>
        <Approval
          id="approve-publish"
          output={outputs.publishApproval}
          request={{ title: "Publish the draft?", summary: "Human review required." }}
          onDeny="continue"
        />
        {decision ? (
          <Task id="record" output={outputs.publishResult}>
            {{ status: decision.approved ? "published" : "rejected" }}
          </Task>
        ) : null}
      </Sequence>
    </Workflow>
  );
});
```

## Notes

- `mode="select"` returns `{ selected, notes }`; `mode="rank"` returns `{ ranked, notes }`.
- Durable deferred keyed on (run, node, iteration) survives restarts; the operating agent resolves it with `bunx smithers-orchestrator approve`/`deny` after relaying the request to the human and getting their decision (never hand the command to the human).
- For a pre-task pause without persisted decision, use `<Task needsApproval>`.

---

## <ApprovalGate>

> Conditional approval; pauses for human when `when` is true, else auto-approves.

```ts
import { ApprovalGate } from "smithers-orchestrator";

type ApprovalGateProps = {
  id: string;
  output: OutputTarget; // z.ZodObject<z.ZodRawShape> | { $inferSelect: Record<string, unknown> } | string
  request: { title: string; summary?: string; metadata?: Record<string, unknown> };
  when: boolean; // true => require human; false => auto-approve immediately
  onDeny?: "fail" | "continue" | "skip"; // if omitted, denial fails the task
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;
  retries?: number;
  retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number };
  continueOnFail?: boolean;
};
```

```tsx
const risk = ctx.output(outputs.riskScore, { nodeId: "risk" });

<Workflow name="deploy-pipeline">
  <Sequence>
    <Task id="risk" output={outputs.riskScore} agent={riskAgent}>
      Assess deploy risk.
    </Task>
    <ApprovalGate
      id="deploy-approval"
      output={outputs.deployDecision}
      when={risk.level === "high"}
      request={{
        title: "Approve high-risk deploy?",
        summary: `Risk score: ${risk.score}/100`,
      }}
      onDeny="fail"
    />
    <Task id="deploy" output={outputs.deploy}>
      {{ deployed: true }}
    </Task>
  </Sequence>
</Workflow>
```

## Notes

- Auto-approve emits a valid `ApprovalDecision` (`{ approved: true, note: "auto-approved", ... }`); downstream branching stays uniform.
- `onDeny` applies only to the human path; auto-approve always succeeds.

---

## <EscalationChain>

> Sequential agent escalation with optional human fallback.

```ts
// Props
import { EscalationChain } from "smithers-orchestrator";

type EscalationChainProps = {
  id?: string; // default "escalation"
  levels: EscalationLevel[];
  humanFallback?: boolean; // default false
  humanRequest?: ApprovalRequest;
  escalationOutput: z.ZodObject | { $inferSelect: Record<string, unknown> } | string;
  skipIf?: boolean;
  children?: ReactNode; // prompt forwarded to every level
};

type EscalationLevel = {
  agent: AgentLike;
  output: z.ZodObject | { $inferSelect: Record<string, unknown> } | string;
  label?: string;
  escalateIf?: (result: unknown) => boolean; // true -> next level
};
```

```tsx
<Workflow name="support-ticket">
  <EscalationChain
    id="support"
    escalationOutput={outputs.escalation}
    humanFallback
    humanRequest={{ title: "Ticket needs human support", summary: "Agents could not resolve." }}
    levels={[
      { agent: fastAgent, output: outputs.tier1, label: "Tier 1", escalateIf: (r) => r.confidence < 0.7 },
      { agent: powerAgent, output: outputs.tier2, label: "Tier 2", escalateIf: (r) => r.confidence < 0.9 },
    ]}
  >
    Resolve this ticket: {ctx.input.ticketBody}
  </EscalationChain>
</Workflow>
```

## Notes

- Each level uses `continueOnFail`; failures propagate to the next level.
- `escalateIf` is evaluated at render time. The chain re-renders reactively as each level's output becomes available and calls the predicate to decide whether the next level mounts.

---

## <DecisionTable>

> Flat rule table that replaces nested Branch trees with declarative routing.

```ts
// Props
import { DecisionTable } from "smithers-orchestrator";

type DecisionTableProps = {
  id?: string;
  rules: DecisionRule[];
  default?: ReactElement; // rendered when no rule matches
  strategy?: "first-match" | "all-match"; // default "first-match"
  skipIf?: boolean;
};

type DecisionRule = {
  when: boolean; // evaluated at render time
  then: ReactElement;
  label?: string;
};
```

```tsx
<DecisionTable
  rules={[
    {
      when: triage.severity === "critical",
      then: (
        <Task id="page-oncall" output={outputs.page} agent={pagerAgent}>
          Page the on-call engineer immediately.
        </Task>
      ),
    },
    {
      when: triage.severity === "high",
      then: <Task id="assign-senior" output={outputs.assign}>{{ assignee: "senior-pool" }}</Task>,
    },
  ]}
  default={<Task id="default-assign" output={outputs.assign}>{{ assignee: "general-pool" }}</Task>}
/>
```

## Notes

- `first-match` builds nested Branches; order matters.
- `all-match` wraps every matching rule in a Parallel; no ordering guarantee.

---

## <HumanTask>

> Suspend until a human submits JSON matching the output schema.

```ts
import { HumanTask } from "smithers-orchestrator";

type HumanTaskProps = {
  id: string;
  output: z.ZodObject | Table | string;
  outputSchema?: z.ZodObject; // inferred when output is a Zod schema
  prompt: string | ReactNode;
  maxAttempts?: number; // default 10
  async?: boolean; // unrelated downstream may continue while pending
  skipIf?: boolean;
  timeoutMs?: number;
  continueOnFail?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
};
```

```tsx
import { Workflow, Sequence, Task, HumanTask, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  review: z.object({
    approved: z.boolean(),
    comments: z.string(),
    severity: z.enum(["low", "medium", "high"]),
  }),
  summary: z.object({ status: z.string() }),
});

export default smithers((ctx) => {
  const review = ctx.outputMaybe(outputs.review, { nodeId: "human-review" });
  return (
    <Workflow name="review-flow">
      <Sequence>
        <HumanTask
          id="human-review"
          output={outputs.review}
          prompt="Review the PR. Provide approved (boolean), comments (string), severity (low|medium|high)."
          maxAttempts={5}
          timeoutMs={86_400_000}
        />
        {review ? (
          <Task id="record" output={outputs.summary}>
            {{ status: review.approved ? "approved" : "changes-requested" }}
          </Task>
        ) : null}
      </Sequence>
    </Workflow>
  );
});
```

## Notes

- The operating agent submits the answer via `bunx smithers-orchestrator human answer <requestId> --value '<json>'`: relay the prompt to the human in conversation, collect their answer, and run the command yourself; never ask the human to run it. Find the requestId with `bunx smithers-orchestrator human inbox`. The `bunx smithers-orchestrator approve` command is for `<Approval>` components, not `<HumanTask>`.
- Failed JSON re-prompts up to `maxAttempts` with zero backoff.
- Same durable deferred mechanism as `<Approval>`; survives restarts.

---

## <Signal>

> Typed wrapper around &lt;WaitForEvent&gt;; signal name equals node id.

```ts
import { Signal } from "smithers-orchestrator";

type SignalProps = {
  id: string; // signal name and node id
  schema: z.ZodObject; // typed payload + output target
  correlationId?: string;
  timeoutMs?: number;
  onTimeout?: "fail" | "skip" | "continue"; // default "fail" (applied by the underlying <WaitForEvent>)
  async?: boolean;
  skipIf?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  smithersContext?: React.Context<SmithersCtx | null>; // advanced: inject a custom Smithers context
  children?: (data) => ReactNode; // mounts only after payload arrives
};
```

```tsx
import { Signal, Task, Workflow, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  feedback: z.object({ rating: z.number(), comment: z.string() }),
  summary: z.object({ upper: z.string() }),
});

export default smithers(() => (
  <Workflow name="signal-demo">
    <Signal id="user-feedback" schema={outputs.feedback} async>
      {(feedback) => (
        <Task id="summarize" output={outputs.summary}>
          {{ upper: feedback.comment.toUpperCase() }}
        </Task>
      )}
    </Signal>
  </Workflow>
));
```

## Notes

- Renders `<WaitForEvent event={id} output={schema}>` internally.
- Async waits show in `smithers_external_wait_async_pending{kind="event"}`.
- Delivering a signal does not resume a suspended run on its own. An external controller should call Gateway `submitSignal`, then `resumeRun`; this keeps auth, ownership, event emission, and backend selection inside the workspace control plane. For ad-hoc CLI operation, send the payload with `bunx smithers-orchestrator signal RUN_ID user-feedback --data '{...}'` (the second argument is the signal name, which equals the `<Signal>` id), then resume the run with `bunx smithers-orchestrator up workflow.tsx --run-id RUN_ID --resume true`.
- Run ad-hoc CLI commands from the workspace root, the directory that holds `.smithers/`. Controllers should discover the verified workspace singleton with `smithers gateway status --format json` and use its URL. Never open or select a backing database to deliver the signal.

---

## <WaitForEvent>

> Durably suspend until a correlated external event arrives, or time out.

```ts
import { WaitForEvent } from "smithers-orchestrator";

type WaitForEventProps = {
  id: string;
  event: string;
  output: z.ZodObject | Table | string;
  correlationId?: string;
  outputSchema?: z.ZodObject;
  timeoutMs?: number;
  onTimeout?: "fail" | "skip" | "continue"; // default "fail"
  async?: boolean; // unrelated downstream may proceed while pending
  skipIf?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
};
```

```tsx
<Workflow name="deploy-watcher">
  <Sequence>
    <WaitForEvent
      id="wait-deploy"
      event="deploy.completed"
      correlationId={ctx.input.deployId}
      output={outputs.deployEvent}
      outputSchema={deployPayload}
      timeoutMs={600_000}
      onTimeout="fail"
    />
    <Task id="notify" output={outputs.summary} agent={notifier}>
      The deploy finished. Summarize the result.
    </Task>
  </Sequence>
</Workflow>
```

## Notes

- Push-based; for poll-based checks use `<Task>` with a compute function.
- External integration webhooks (the `@smithers-orchestrator/integrations` package) deliver these events. A source signals `integration:<service>:<event>` (for example `integration:github:pull_request`) with a `correlationId` that is `owner/repo#number`, `owner/repo`, or `null` depending on the payload, so set `signalName` and `correlationId` to match.
- With `async`, dependents via `dependsOn`/`needs` still block until payload arrives.
- Async waits are tracked by the `smithers_external_wait_async_pending{kind="event"}` gauge while pending (it rises on start, falls on completion).
- A run suspended on `<WaitForEvent>` (status `waiting-event`) does not resume itself when the event is delivered out of process. A controller should deliver it with Gateway `submitSignal`, then call `resumeRun`. For ad-hoc CLI operation, deliver the event and resume with `bunx smithers-orchestrator up workflow.tsx --run-id RUN_ID --resume true` from the workspace root. Never locate or select the backing store yourself; discover the verified workspace Gateway with `smithers gateway status --format json`. See the keeper-loop pattern in the CLI overview.

---

## <Timer>

> Durably suspend for a relative duration or until an absolute time.

```ts
import { Timer } from "smithers-orchestrator";

type TimerProps = {
  id: string;
  duration?: string; // "500ms" | "30s" | "2h" | "7d"; exactly one of duration/until required
  until?: string | Date; // ISO 8601 string or Date
  every?: string; // reserved: recurring timers ship in phase 2; throws if set
  skipIf?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
};
```

```tsx
<Workflow name="delayed-report">
  <Sequence>
    <Timer id="cooldown" duration="30s" />
    <Task id="report" output={outputs.report} agent={reportAgent}>
      Generate the daily summary report.
    </Task>
  </Sequence>
</Workflow>
```

## Notes

- Exactly one of `duration` or `until` is required; both or neither throws at render time.
- Produces no output. Past `until` timestamps fire immediately.
- Worker restarts during the wait don't reset the timer.

## Durable Suspend and Wake

A waiting timer holds no worker and no CPU. The run's status becomes `waiting-timer`, only the absolute fire time is persisted, and the host releases the worker. Tear the worker down or redeploy mid-wait and nothing is lost.

The host wakes the run on its own when the fire time arrives. A Gateway sweeps due timers on its scheduler tick (every 1 to 15s), and `bunx smithers-orchestrator supervise` also scans `waiting-timer` runs and resumes due ones when you are operating without Gateway. Wake resolution is bounded by the Gateway tick or supervisor interval rather than by a live process. Timers that came due while the host was down fire on the first sweep after it restarts.

---

## <Saga>

> Forward steps with compensations run in reverse on failure.

```ts
import { Saga } from "smithers-orchestrator";

type SagaStepDef = {
  id: string;
  action: ReactElement;
  compensation: ReactElement;
  label?: string;
};

type SagaProps = {
  id?: string;
  steps?: SagaStepDef[]; // takes priority over children
  onFailure?: "compensate" | "compensate-and-fail" | "fail"; // default "compensate"
  skipIf?: boolean;
  children?: React.ReactNode; // alternative: <Saga.Step> children
};
```

```tsx
<Workflow name="deploy-saga">
  <Saga
    id="deploy"
    steps={[
      {
        id: "create-pr",
        action: <Task id="create-pr" output={outputs.pr} agent={codeAgent}>Create a PR.</Task>,
        compensation: <Task id="close-pr" output={outputs.closePr} agent={codeAgent}>Close the PR.</Task>,
      },
      {
        id: "deploy-staging",
        action: <Task id="deploy-staging" output={outputs.staging} agent={deployAgent}>Deploy staging.</Task>,
        compensation: <Task id="rollback-staging" output={outputs.rollbackStaging} agent={deployAgent}>Rollback staging.</Task>,
      },
    ]}
  />
</Workflow>
```

## Notes

- Steps run sequentially; compensations run in reverse from the failed step.
- Compensations should be idempotent, and each one **must actually undo its step's action**; a stubbed or inert compensation yields a green run with dirty state on rollback.
- `steps` and `<Saga.Step>` children are mutually exclusive; `steps` wins if both are given.
- `SagaStep` is a named export (`import { SagaStep }`) and is the same marker as `<Saga.Step>`.

---

## <TryCatchFinally>

> Workflow-scoped error boundary with catch handlers and guaranteed cleanup.

```ts
import { TryCatchFinally } from "smithers-orchestrator";

type TryCatchFinallyProps = {
  id?: string;
  try: ReactElement;
  catch?: ReactElement | ((error: SmithersError) => ReactElement);
  catchErrors?: SmithersErrorCode[]; // restrict which codes trigger catch
  finally?: ReactElement; // always runs after try or catch
  skipIf?: boolean;
};
```

```tsx
<Workflow name="safe-deploy">
  <TryCatchFinally
    try={
      <Sequence>
        <Task id="build" output={outputs.build} agent={buildAgent}>Build the project.</Task>
        <Task id="deploy" output={outputs.deploy} agent={deployAgent}>Deploy to production.</Task>
      </Sequence>
    }
    catch={(error) => (
      <Task id="recover" output={outputs.recover} agent={recoveryAgent}>
        {`Recover from ${error.code}: ${error.summary}`}
      </Task>
    )}
    finally={
      <Task id="cleanup" output={outputs.cleanup}>{{ cleanedUp: true }}</Task>
    }
  />
</Workflow>
```

## Notes

- `try` takes one `ReactElement`; wrap multiples in `<Sequence>` or `<Parallel>`.
- `finally` runs even if `catch` fails.
- Unmatched `catchErrors` propagate to outer boundaries.

---

## <Sandbox>

> Run a child workflow through an injectable sandbox provider and collect outputs, artifacts, and reviewed file changes.

`<Sandbox>` is a task boundary for work that should execute outside the parent task process. The public component is provider-first: pass an injectable provider object or a registered provider id. `runtime` remains only for the built-in legacy local transports.

```ts
import { Sandbox } from "smithers-orchestrator";
import type { SandboxProvider } from "smithers-orchestrator/sandbox";

type SandboxProps = {
  id: string;
  output: ZodObject | DrizzleTable | string;
  workflow?: SmithersWorkflow<unknown>;
  input?: unknown;

  provider?: unknown; // runtime accepts a provider object or registered provider id
  runtime?: "bubblewrap" | "docker" | "codeplane" | "cloudflare"; // legacy local transports plus provider-backed Cloudflare

  allowNetwork?: boolean;
  reviewDiffs?: boolean; // default true
  autoAcceptDiffs?: boolean; // default false
  allowNested?: boolean; // default false

  image?: string;
  env?: Record<string, string>;
  egress?: {
    env?: Record<string, string>; // HTTP_PROXY, HTTPS_PROXY, NO_PROXY, etc.
    httpProxy?: string;
    httpsProxy?: string;
    noProxy?: string | string[];
    caCertPem?: string; // materialized into the request bundle
    caCertPath?: string; // path that already exists inside the sandbox
    secretBindings?: Record<string, string>;
  };
  ports?: Array<{ host: number; container: number }>;
  volumes?: Array<{ host: string; container: string; readonly?: boolean }>;
  memoryLimit?: string;
  cpuLimit?: string;
  command?: string;
  workspace?: {
    name: string;
    snapshotId?: string;
    idleTimeoutSecs?: number;
    persistence?: "ephemeral" | "sticky";
  };
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;
  retries?: number;
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
  cache?: CachePolicy;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: ReactNode;
};
```

The sandbox provider contract types are public exports from
`"smithers-orchestrator/sandbox"`: `SandboxProvider`,
`SandboxProviderRequest`, `SandboxProviderResult`, and
`ExecuteSandboxOptions`. Import them with `import type` when writing adapters.

## Egress controls

Use `egress` when the sandbox itself should own outbound-network configuration. Smithers does not mutate the parent harness environment for this path. It serializes the egress contract into the sandbox request and passes it to the selected provider or local transport.

```tsx
<Sandbox
  id="review"
  provider={remoteVmProvider}
  workflow={reviewWorkflow}
  input={{ ticketId: ctx.input.ticketId }}
  output={outputs.result}
  allowNetwork
  egress={{
    env: {
      ANTHROPIC_API_KEY: "sk-proxy-anthropic",
    },
    httpsProxy: "http://127.0.0.1:8080",
    httpProxy: "http://127.0.0.1:8080",
    noProxy: ["127.0.0.1", "localhost"],
    caCertPem: process.env.IRON_PROXY_CA_PEM,
    secretBindings: {
      "sk-proxy-anthropic": "anthropic",
    },
  }}
/>
```

Provider-backed sandboxes receive the normalized contract as `request.egress`, and the raw serializable value remains in `request.config.egress` for adapters that need provider-specific fields. The JSX prop is typed `unknown`; at execution time Smithers accepts a provider object directly or a string id only after registering a provider with `registerSandboxProvider({ id, run })`. The `unknown` type lets provider packages supply their own shapes, so TypeScript will not reject a bad provider at the JSX call site; invalid provider values fail during execution with `INVALID_INPUT`. Smithers core does not hardcode a Freestyle or iron-proxy sandbox adapter. Local transports merge the generated proxy environment into the sandbox handle environment, after `env`, so `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, and `NODE_EXTRA_CA_CERTS` are visible inside the sandbox process.

When `caCertPem` is provided, Smithers writes it into the request bundle at `.smithers/egress/ca.crt` and points local transports at `/workspace/.smithers/egress/ca.crt`. If the CA is already installed by the sandbox image or sidecar, use `caCertPath` instead. Persisted sandbox config redacts egress env values, CA PEM, and secret-binding entries.

## Basic usage

```tsx
const provider = {
  id: "remote-vm",
  async run(request) {
    const remote = await createRemoteVm({
      input: request.input,
      requestBundlePath: request.requestBundlePath,
    });

    return {
      status: "finished",
      output: await remote.readJson("/workspace/smithers-result.json"),
      remoteRunId: remote.id,
      workspaceId: remote.workspaceId,
      diffBundle: await remote.diffBundle(),
    };
  },
};

<Workflow name="remote-codegen">
  <Sandbox
    id="generate"
    provider={provider}
    workflow={generateCodeWorkflow}
    input={{ prompt: ctx.input.prompt }}
    output={outputs.result}
    allowNetwork={false}
    reviewDiffs
  />
</Workflow>
```

`workflow` takes a `SmithersWorkflow`, the same value a workflow file's default
export holds. `smithers((ctx) => ...)` returns a `SmithersWorkflow`, so the child
is a normal `createSmithers` module you import; no extra wrapping is needed:

```tsx
// child workflow, defined and exported like any other
const child = createSmithers({
  input: z.object({ prompt: z.string() }),
  result: z.object({ summary: z.string() }),
});

export const generateCodeWorkflow = child.smithers((ctx) => (
  <child.Workflow name="generate-code">
    <child.Task id="code" output={child.outputs.result} agent={coder}>
      {`Generate code for: ${ctx.input.prompt}`}
    </child.Task>
  </child.Workflow>
));
```

The provider's `run()` returns the result your parent receives. The minimal
structured shape is `{ status: "finished", output, remoteRunId?, workspaceId? }`,
where `output` matches the `output` schema (`outputs.result` above). Add a
`diffBundle` when the sandbox changed files (see Result bundles).

## Complete one-file provider and child workflow

This is the smallest complete pattern: a concrete provider object,
`allowNetwork={true}`, and a child workflow in the same file. Real providers
usually create a VM or container, but they still return the same structured
`SandboxProviderResult`.

```tsx
import { createSmithers, Sandbox } from "smithers-orchestrator";
import type { SandboxProvider } from "smithers-orchestrator/sandbox";
import { z } from "zod";

const child = createSmithers({
  input: z.object({ prompt: z.string() }),
  result: z.object({ summary: z.string() }),
});

const childWorkflow = child.smithers((ctx) => (
  <child.Workflow name="sandbox-child">
    <child.Task id="summarize" output={child.outputs.result}>
      {{ summary: `Processed: ${ctx.input.prompt}` }}
    </child.Task>
  </child.Workflow>
));

const provider: SandboxProvider = {
  id: "local-demo-provider",
  async run(request) {
    request.heartbeat({ phase: "starting-child" });

    const childRun = await request.executeChildWorkflow(request.parentWorkflow, {
      workflow: request.workflow,
      input: request.input,
      parentRunId: request.runId,
      rootDir: request.rootDir,
      allowNetwork: request.allowNetwork,
      signal: request.signal,
    });

    return {
      status: childRun.status === "finished" ? "finished" : "failed",
      output: childRun.output,
      remoteRunId: childRun.runId,
      workspaceId: request.sandboxId,
    };
  },
};

const parent = createSmithers({
  input: z.object({ prompt: z.string() }),
  result: z.object({ summary: z.string() }),
});

export default parent.smithers((ctx) => (
  <parent.Workflow name="sandbox-parent">
    <Sandbox
      id="remote-summary"
      provider={provider}
      workflow={childWorkflow}
      input={{ prompt: ctx.input.prompt }}
      output={parent.outputs.result}
      allowNetwork={true}
    />
  </parent.Workflow>
));
```

## Execution model

1. Smithers renders `<Sandbox>` as one scheduler task. Children do not become parent-run tasks.
2. At execution time Smithers writes a request bundle under `.smithers/sandboxes/<run>/<sandbox>/request-bundle`.
3. A provider receives the request, runs work remotely, and returns either a local bundle path or a structured result.
4. Smithers validates the result bundle, records sandbox lifecycle events, enforces diff review policy, applies accepted `diffBundle`s, and returns `outputs` to the parent task output table.

The provider contract receives the child workflow, input, root directory, request/result bundle paths, limits, abort signal, and a heartbeat callback:

```ts
type SandboxProviderRequest = {
  runId: string;
  sandboxId: string;
  input?: unknown;              // exactly the <Sandbox input={...}> value
  rootDir: string;
  requestBundlePath: string;
  resultBundlePath: string;
  workflow: SandboxChildWorkflowDefinition;
  parentWorkflow?: SandboxWorkflow;
  executeChildWorkflow: ExecuteSandboxChildWorkflow;
  allowNetwork: boolean;
  maxOutputBytes: number;
  toolTimeoutMs: number;
  egress?: SandboxEgressConfig;
  config: Record<string, unknown>;
  signal?: AbortSignal;
  heartbeat(data?: unknown): void;
};

type SandboxProvider = {
  id: string;
  run(request: SandboxProviderRequest): Promise<SandboxProviderResult> | SandboxProviderResult;
  cleanup?(request: SandboxProviderRequest): Promise<void> | void;
};
```

Register reusable providers when a workflow should reference them by id:

```ts
import { registerSandboxProvider } from "smithers-orchestrator/sandbox";

const unregister = registerSandboxProvider(provider);

<Sandbox id="generate" provider={provider.id} workflow={child} output={outputs.result} />;
```

## Result bundles

A provider can return a path to a bundle it created:

```ts
return {
  bundlePath: "/tmp/smithers-result-bundle",
  remoteRunId: vmId,
  workspaceId: vmId,
};
```

Or it can return a structured result and let Smithers materialize the bundle locally:

```ts
return {
  status: "finished",
  output: { summary: "done" },
  runId: vmId,
  diffBundle: {
    seq: 1,
    baseRef: "HEAD",
    patches: [
      {
        path: "src/app.ts",
        operation: "modify",
        diff: "diff --git a/src/app.ts b/src/app.ts\n...",
      },
    ],
  },
};
```

Use the structured-result form when your adapter can return the output JSON and
optional `diffBundle` directly. Use `bundlePath` when the remote side already
wrote a full Smithers result bundle, when you need to preserve larger artifacts,
or when the provider owns bundle materialization.

Bundle limits are enforced before the result is accepted: 100 MB total, 5 MB manifest file (README.md), 1,000 patch files, bounded JSON output, and no path traversal or symlinks in bundle paths.

## Diff review

`reviewDiffs` defaults to `true`. If the sandbox returns patch files or a `diffBundle`, Smithers records `SandboxDiffReviewRequested`.

When `autoAcceptDiffs` is false, changed bundles fail closed until a review path accepts them. When `autoAcceptDiffs` is true, or `reviewDiffs` is false, Smithers applies `diffBundle` through the engine diff-bundle applier. Legacy patch files are still collected and review-gated, but the apply path is `diffBundle`.

## Nested sandboxes

Nested sandbox execution is disabled by default. A sandbox running inside another sandbox must set `allowNested`.

Use nesting only when the provider and diff policy are designed for it. The hard cases are:

- Diff base conflicts: an inner sandbox can generate a `diffBundle` against a different base than the outer sandbox.
- Cleanup ordering: an outer provider cleanup can delete the workspace before the inner provider finishes.
- Quotas and concurrency: nested remote VMs can multiply resource usage quickly.
- Network and secrets: inherited remote credentials may be broader than intended.
- Event lineage: parent run, outer sandbox run, and inner sandbox run need clear ids for debugging.

For most workflows, use sibling sandboxes under a `Parallel` or `MergeQueue` instead of nesting.

## Built-in local transports

When `provider` is omitted, Smithers uses the legacy local transport path. `runtime` may be `"bubblewrap"`, `"docker"`, or `"codeplane"`. `"cloudflare"` is provider-backed; use it with `createCloudflareSandboxProvider()` from `smithers-orchestrator/cloudflare`. If `runtime` is omitted, the local path defaults to `"bubblewrap"`.

Unknown runtimes now fail closed. Docker is not silently replaced by bubblewrap when Docker is unavailable.

## First-class cloud providers

Smithers ships first-class sandbox providers for five cloud backends. Each is a
separate optional package that maps the provider contract onto a vendor SDK, so
`<Sandbox provider={createDaytonaSandboxProvider({...})}>` runs the child
workflow on that backend with no runtime change. See
Sandbox Providers for the shared contract, the
provider-kit, selection precedence, and a comparison table.

- Daytona Sandbox Provider: id `daytona-sandbox`
- Vercel Sandbox Provider: id `vercel-sandbox`
- AWS Sandbox Provider: id `aws-sandbox` (Fargate or CodeBuild, S3 transport)
- GCP Sandbox Provider: id `gcp-sandbox` (Cloud Run Jobs, GCS transport)
- Cloudflare: id `cloudflare-sandbox`

## Freestyle provider example

[Freestyle VMs](https://www.freestyle.sh/docs/vms) are full sandboxes with nested virtualization, full networking, and the ability to scale to more resources than alternatives. Use Freestyle VMs when you want to give your agents a real computer rather than a code runner.

See `examples/freestyle/` for a provider adapter that creates a Freestyle VM, writes setup and request files with `vm.fs.writeTextFile()`, executes a command with `vm.exec()`, reads `smithers-result.json` with `vm.fs.readTextFile()`, and returns a Smithers sandbox result.

Freestyle's current VM docs show the stable package as `freestyle`, VM creation through `freestyle.vms.create()`, file I/O through `vm.fs.writeTextFile()` / `vm.fs.readTextFile()`, command execution with `vm.exec()`, and lifecycle controls such as `start()`, `stop()`, `resize()`, `fork()`, `delete({ vmId })`, and `idleTimeoutSeconds`. Relevant Freestyle docs:

- https://www.freestyle.sh/docs/vms
- https://www.freestyle.sh/docs/vms/lifecycle

---

## Sandbox Providers

> The SandboxProvider contract, the shared provider-kit, provider selection and precedence, egress and secret redaction, and the built-in cloud providers.


A `<Sandbox>` boundary runs a child workflow outside the
parent task process. Where it runs is decided by a provider. A provider is a
plain object that takes a request and returns a result. Smithers ships
first-class providers for five cloud backends and a shared kit so a new provider
is a thin adapter.

The built-in providers are Daytona, Vercel, AWS, GCP, and Cloudflare. Each is a
separate optional package with its own SDK as an optional dependency, so you
install only what you use.

## The SandboxProvider contract

A provider implements `run()` and an optional `cleanup()`:

```ts
type SandboxProvider = {
  id: string;
  run(request: SandboxProviderRequest): Promise<SandboxProviderResult> | SandboxProviderResult;
  cleanup?(request: SandboxProviderRequest): Promise<void> | void;
};
```

`run()` receives the child workflow, the `<Sandbox input>` value, the request and
result bundle paths, the network and output-size limits, an abort signal, and a
heartbeat callback. It returns either a `bundlePath` to a bundle it wrote, or a
structured `{ status, output, remoteRunId?, workspaceId?, diffBundle? }`. Smithers
validates the result, enforces diff-review policy, and returns the outputs to the
parent task. See `<Sandbox>` for the full request and
result types.

## The provider-kit

Cloud providers do not implement `run()` from scratch. They call
`createCommandSandboxProvider` from `smithers-orchestrator/sandbox`, which owns
the request/result-file protocol once so every provider behaves the same:

1. Create a vendor session and emit a `session-created` heartbeat.
2. Write the request JSON, carrying only the safe fields (`runId`, `sandboxId`,
   `input`, `config`, `allowNetwork`, `maxOutputBytes`, redacted egress).
3. Merge the command env: `options.env`, the egress env, and the two path vars
   below. It never copies arbitrary `process.env`.
4. Run the entry command with the request's `toolTimeoutMs` and abort signal.
5. Parse the result from stdout if it starts with `{`, otherwise read the result
   file. A nonzero exit with no valid result JSON throws
   `SANDBOX_EXECUTION_FAILED` with stdout and stderr truncated to
   `maxOutputBytes` and redacted.
6. On `cleanup`, resolve the cached session and destroy it per policy.

A provider author implements a small `SandboxSession` seam, not the protocol:

```ts
type SandboxSession = {
  readonly remoteId: string;
  readonly writeFile: (path: string, content: string) => Promise<void>;
  readonly readFile: (path: string) => Promise<string>;
  readonly exec: (command: string, opts: SandboxExecOptions) => Promise<SandboxExecResult>;
  readonly destroy?: () => Promise<void>;
};
```

The kit hands the entry command two env vars:

- `SMITHERS_SANDBOX_REQUEST_PATH`: where to read the request JSON.
- `SMITHERS_SANDBOX_RESULT_PATH`: where to write the result JSON.

The entry command either prints the result JSON to stdout or writes it to
`SMITHERS_SANDBOX_RESULT_PATH`. Providers with no shared filesystem (AWS, GCP)
transport those files through S3 or GCS and inject extra vars pointing the entry
at the object keys.

## Selecting a provider

There are three ways to pick a provider, in precedence order:

1. **Provider object (primary).** Pass the factory result straight to the prop:
   `<Sandbox provider={createDaytonaSandboxProvider({...})} workflow={child} />`.
2. **Registered id.** Register once, then reference by id:
   `registerSandboxProvider(createVercelSandboxProvider({...}))` then
   `<Sandbox provider="vercel-sandbox" />`. The provider packages also export
   `register<Provider>SandboxProvider(...)` convenience wrappers.
3. **Env default (planned, not yet shipped).** A future `SMITHERS_SANDBOX_PROVIDER`
   env var will select a registered id when a `<Sandbox>` sets no explicit
   `provider`, resolving a registered id only (never auto-creating a cloud
   client). It is not implemented yet; setting it today has no effect. The
   intended precedence, once shipped, mirrors the DB-backend chain: an explicit
   `provider` prop wins, then workflow config, then `SMITHERS_SANDBOX_PROVIDER`,
   then the local default. Until then, select a provider with method 1 or 2.

Credentials always come from factory options first and the vendor env chain
second. They are never required in `request.config`.

## Egress and secret redaction

Egress is configured on the `<Sandbox egress>` prop and reaches the provider as
the normalized `request.egress`. The kit projects it into the command env
through `sandboxEgressEnv()`, and when `caCertPem` is set it uploads the CA to
the workspace so `NODE_EXTRA_CA_CERTS` resolves inside the sandbox. See
`<Sandbox>` egress controls.

Providers with a real remote filesystem (Daytona, Vercel, Cloudflare) resolve
that CA path directly. The object-store providers (AWS, GCP) have no shared
filesystem, so they upload the CA to their bundle store and pass its location to
the container: AWS injects `SMITHERS_SANDBOX_CA_S3_KEY` and GCP injects
`SMITHERS_SANDBOX_CA_GCS_OBJECT`. The container entry command must download that
object and write it to the path named by `NODE_EXTRA_CA_CERTS` before it makes
outbound calls.

Credentials are used only to build the local SDK client. They are never placed
in the request JSON and never forwarded into the remote env unless you list them
explicitly in `options.env`. `redactSandboxProviderValue()` redacts keys
containing `token`, `secret`, `key`, `password`, `credential`, or
`authorization` from every thrown message, heartbeat payload, and the persisted
`configJson` audit row.

## Provider comparison

| Provider | Package | Provider id | Compute | Bundle transport | Auth | Exit code |
|---|---|---|---|---|---|---|
| Daytona | `smithers-orchestrator/daytona` | `daytona-sandbox` | Daytona sandbox | Sandbox filesystem | `DAYTONA_API_KEY` | numeric |
| Vercel | `smithers-orchestrator/vercel` | `vercel-sandbox` | Vercel Sandbox | Sandbox filesystem | OIDC or token trio | numeric |
| AWS | `smithers-orchestrator/aws` | `aws-sandbox` | Fargate or CodeBuild | S3 bucket (BYO) | AWS credential chain | Fargate numeric, CodeBuild status |
| GCP | `smithers-orchestrator/gcp` | `gcp-sandbox` | Cloud Run Jobs | GCS bucket (BYO) | Application Default Credentials | task status (0/1) |
| Cloudflare | `smithers-orchestrator/cloudflare` | `cloudflare-sandbox` | Sandbox SDK container | Sandbox filesystem | Durable Object binding | numeric |

Per-provider setup, options, and cost live on each provider page:

- Daytona Sandbox Provider
- Vercel Sandbox Provider
- AWS Sandbox Provider
- GCP Sandbox Provider
- Cloudflare

---

## <Subflow>

> Invoke a child workflow with its own retry, cache, resume boundary.

```ts
import { Subflow } from "smithers-orchestrator";

type SubflowProps = {
  id: string;
  workflow: SmithersWorkflow<unknown>;
  output: OutputTarget;
  input?: unknown;
  mode?: "childRun" | "inline"; // default "childRun"
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;
  retries?: number;
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
  cache?: CachePolicy;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: React.ReactNode;
};
```

`Subflow` is a top-level component export. `createSmithers(...)` returns typed
`Workflow`, `Task`, `Approval`, `Sandbox`, `Signal`, and pure structural helpers
such as `Branch`, `Loop`, and `Parallel`; import `Subflow` from
`"smithers-orchestrator"` when you need a child workflow boundary.

The `workflow` prop takes a `SmithersWorkflow` value, which is exactly what the
default export of a workflow file is. `smithers((ctx) => ...)` returns a
`SmithersWorkflow`, so a child workflow is just another `createSmithers` module:

```tsx
// child.tsx - a normal workflow file. Its default export is a SmithersWorkflow.
import { createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const child = createSmithers({
  input: z.object({ repo: z.string() }),     // types ctx.input inside the child
  childResult: z.object({ summary: z.string() }),
});

const childWorkflow = child.smithers(
  (ctx) => (
    <child.Workflow name="scan-repo">
      <Sequence>
        <Task id="scan" output={child.outputs.childResult} agent={scanner}>
          {`Scan ${ctx.input.repo} and summarize.`}
        </Task>
      </Sequence>
    </child.Workflow>
  ),
  { output: child.outputs.childResult },
);

export default childWorkflow;
```

The second argument to `smithers(build, { output })` declares which schema
becomes the child's `RunResult.output`: the value a parent `<Subflow>`
persists. Pass the `createSmithers(...).outputs.<key>` schema (a string schema
key or a Drizzle table also work). Without it, the child result is read only
from a schema key literally named `output`, so a child that writes to any other
key (like `childResult` here) would yield an empty subflow result. Declare it
whenever the child's result schema is not named `output`.

Import that value into the parent and hand it to `<Subflow workflow={...}>`.
The parent declares the schema it wants to persist the subflow result under;
that key does not have to match the child's schema key, but the shape must match
the child result you expect:

```tsx
import { createSmithers, Sequence, Subflow, Task } from "smithers-orchestrator";
import childWorkflow from "./child.tsx";
import { z } from "zod";

const parent = createSmithers({
  childSummary: z.object({ summary: z.string() }),
  finalResult: z.object({ title: z.string(), summary: z.string() }),
});

export default parent.smithers((ctx) => {
  const child = ctx.outputMaybe(parent.outputs.childSummary, { nodeId: "run-child" });

  return (
    <parent.Workflow name="parent-flow">
      <Sequence>
        <Subflow
          id="run-child"
          workflow={childWorkflow}
          input={{ repo: "acme/app" }}
          output={parent.outputs.childSummary}
          retries={2}
          timeoutMs={300_000}
        />

        {child ? (
          <Task id="summarize" output={parent.outputs.finalResult} agent={summarizer}>
            {`Summarize the child workflow result: ${child.summary}`}
          </Task>
        ) : null}
      </Sequence>
    </parent.Workflow>
  );
});
```

The child reads the `input` you pass via its own `ctx.input`. Type it by giving
the child's `createSmithers` an `input` schema (above); without one, `ctx.input`
is untyped and you must guard each field (`ctx.input?.repo ?? "."`).

Read the persisted subflow result in the parent exactly like a task output:
`ctx.outputMaybe(parent.outputs.childSummary, { nodeId: "run-child" })`. The
`nodeId` is the `<Subflow id>`, and the output target is the parent's schema key.

## Single-file parent and child

For small examples and eval fixtures, define both factories in one `.tsx` file.
Use separate `createSmithers` calls so the child has its own `ctx.input` type and
output refs, and the parent has its own output table for the subflow result:

```tsx
import { createSmithers, Subflow } from "smithers-orchestrator";
import { z } from "zod";

const child = createSmithers({
  input: z.object({ repo: z.string() }),
  scanResult: z.object({ summary: z.string() }),
});

const scanWorkflow = child.smithers(
  (ctx) => (
    <child.Workflow name="scan-child">
      <child.Task id="scan" output={child.outputs.scanResult}>
        {{ summary: `Scanned ${ctx.input.repo}` }}
      </child.Task>
    </child.Workflow>
  ),
  { output: child.outputs.scanResult },
);

const parent = createSmithers({
  input: z.object({ repo: z.string() }),
  childSummary: z.object({ summary: z.string() }),
  finalResult: z.object({ message: z.string() }),
});

export default parent.smithers((ctx) => {
  const childSummary = ctx.outputMaybe(parent.outputs.childSummary, {
    nodeId: "run-scan",
  });

  return (
    <parent.Workflow name="parent-flow">
      <parent.Sequence>
        <Subflow
          id="run-scan"
          workflow={scanWorkflow}
          input={{ repo: ctx.input.repo }}
          output={parent.outputs.childSummary}
        />

        {childSummary ? (
          <parent.Task id="finish" output={parent.outputs.finalResult}>
            {{ message: `Child said: ${childSummary.summary}` }}
          </parent.Task>
        ) : null}
      </parent.Sequence>
    </parent.Workflow>
  );
});
```

The parent's `input` schema is optional but recommended whenever the parent
reads `ctx.input.repo`; it turns `ctx.input` from `unknown` into a typed object.
The child has its own `input` schema and its own `ctx.input`, populated from the
value passed through the parent's `<Subflow input={...}>` prop.

## Notes

- `childRun` (default) gives the child its own DB row; retry/cache/resume scope it as a unit.
- `inline` renders the child tree as siblings in the parent plan, sharing its scope.
- Subflows compose; children may contain `<Subflow>` themselves.
- Declare the child's result via `smithers(build, { output })` unless its result schema is literally named `output`; that is the schema a parent `<Subflow>` persists.

---

## <ContinueAsNew>

> Close the current run; start a fresh one with optional carried state.

```ts
import { ContinueAsNew, continueAsNew } from "smithers-orchestrator";

type ContinueAsNewProps = {
  state?: unknown; // JSON-serializable; arrives as ctx.input.__smithersContinuation.payload
};

// continueAsNew(state?) is a helper equivalent to <ContinueAsNew state={state} />
```

```tsx
export default smithers((ctx) => {
  const continuation = (ctx.input as any)?.__smithersContinuation as
    | { payload?: { cursor?: string; count?: number } }
    | undefined;

  const cursor = continuation?.payload?.cursor ?? null;
  const count = continuation?.payload?.count ?? 0;

  return (
    <Workflow name="paginated-processor">
      <Sequence>
        <Task id="process-batch" output={outputs.processed} agent={processorAgent}>
          {`Process next page. Cursor: ${cursor ?? "start"}. Total so far: ${count}.`}
        </Task>
        <ContinueAsNew
          state={{
            cursor: ctx.outputMaybe(outputs.processed, { nodeId: "process-batch" })?.lastCursor,
            count: count + 1,
          }}
        />
      </Sequence>
    </Workflow>
  );
});
```

## Notes

- `state` must be JSON-serializable; total continuation envelope < 10 MB.
- Workflow id is preserved across continuations; only run id increments.
- Nodes rendered after `<ContinueAsNew>` in the same sequence don't execute.

---

## <SuperSmithers>

> Reads and modifies source code at runtime via a markdown strategy doc.

```ts
import { SuperSmithers } from "smithers-orchestrator";

type SuperSmithersProps = {
  strategy: string | ReactElement; // markdown or MDX strategy document
  agent: AgentLike;
  id?: string; // default "super-smithers"; prefixes internal task ids
  targetFiles?: string[]; // glob patterns scoped to the agent prompt
  reportOutput?: OutputTarget;
  dryRun?: boolean; // default false; skips the apply step
  skipIf?: boolean;
};
```

```tsx
<Workflow name="intervention">
  <SuperSmithers
    id="refactor"
    strategy={`
      ## Refactoring Strategy
      1. Find all deprecated API calls in the target files
      2. Replace them with the new API equivalents
      3. Ensure all imports are updated
    `}
    agent={codeAgent}
    targetFiles={["src/**/*.ts"]}
    reportOutput={outputs.report}
  />
</Workflow>
```

## Notes

- Expands to read → propose → apply → report (`dryRun` skips apply).
- Only meaningful in hot-reload mode; otherwise changes apply on the next run.
- All internal tasks share `agent`; for per-stage agents compose `<Task>` manually.

---

## <Aspects>

> Propagate token and latency budgets to descendant tasks.

```ts
import { Aspects } from "smithers-orchestrator";

type TokenBudgetConfig = {
  max: number;
  perTask?: number;
  onExceeded?: "fail" | "warn" | "skip-remaining"; // default "fail"
};

type LatencySloConfig = {
  maxMs: number;
  perTask?: number;
  onExceeded?: "fail" | "warn"; // default "fail"
};

type TrackingConfig = { tokens?: boolean; latency?: boolean };

type AspectsProps = {
  tokenBudget?: TokenBudgetConfig;
  latencySlo?: LatencySloConfig;
  tracking?: TrackingConfig; // default all true
  children?: ReactNode;
};
```

```tsx
<Workflow name="budgeted-workflow">
  <Aspects
    tokenBudget={{ max: 100_000, perTask: 25_000, onExceeded: "warn" }}
    latencySlo={{ maxMs: 30_000, onExceeded: "fail" }}
  >
    <Task id="analyze" output={outputs.analysis} agent={codeAgent}>
      Analyze the repository.
    </Task>
    <Task id="review" output={outputs.review} agent={reviewAgent}>
      Review the analysis.
    </Task>
  </Aspects>
</Workflow>
```

## Notes

- Nested `<Aspects>` inherit outer budget configs wholesale. If an inner `<Aspects>` sets `tokenBudget` or `latencySlo`, that field's entire parent config object is replaced (no per-field merge). The `tracking` config is the exception: `tokens` and `latency` each fall back to the parent value independently.
- Budgets enforce at task-dispatch time. Before each descendant task runs, the engine compares the run's accumulated token total (`tokenBudget.max`) and wall-clock since the run started (`latencySlo.maxMs`) against the budget and applies `onExceeded`: `fail` raises `ASPECT_BUDGET_EXCEEDED` and fails the run, `warn` logs and continues, `skip-remaining` skips the task. The per-task limits (`perTask`) are not enforced yet.
- Budgets enforce regardless of `tracking`; `tracking` controls metric emission only.
- Accumulated token usage is per run and survives resume (re-seeded from persisted token-usage events).

---

## <Worktree>

> Run a subtree in an isolated worktree (git or jj) rooted at `path`.

```ts
import { Worktree } from "smithers-orchestrator";

type WorktreeProps = {
  key?: string;
  path: string; // required, non-empty; relative paths resolve against the launch root
  id?: string;
  branch?: string; // omit to use current branch
  baseBranch?: string; // default "main"
  skipIf?: boolean;
  children?: ReactNode;
};
```

```tsx
<Worktree path="/tmp/smithers/wt-a" baseBranch="main">
  <Task id="build" output={outputs.outputC}>{{ value: 1 }}</Task>
  <Task id="test" output={outputs.outputC}>{{ value: 2 }}</Task>
  <MergeQueue>
    <Task id="apply" output={outputs.outputC}>{{ value: 3 }}</Task>
  </MergeQueue>
  <Parallel maxConcurrency={2}>
    <Task id="lint" output={outputs.outputC}>{{ value: 4 }}</Task>
  </Parallel>
</Worktree>
```

## Path Access

Workflow code can read the same resolved absolute worktree path that Smithers uses for task
execution:

```tsx
const root = ctx.worktreePath("build") ?? ctx.resolveWorktreePath("/tmp/smithers/wt-a");
const verify = JSON.parse(readFileSync(join(root, "artifacts/build/verify.json"), "utf8"));
```

Use `ctx.worktreePath(taskOrWorktreeId)` when a task descriptor or explicit `<Worktree id>`
has rendered. Use `ctx.resolveWorktreePath(pathProp)` for the same string passed to
`<Worktree path={...}>`, including on the first render before descriptor lookups exist.
Both APIs use the graph resolver, so they match the path Smithers gives worker tasks.

## Notes

- Descendants inherit `worktreeId` and absolute `worktreePath` as `cwd`.
- **Do not pin a `cwd` on an agent used inside a `<Worktree>`.** A worker agent's working
  directory resolves as `agent.cwd ?? worktreePath ?? repoRoot`, so an explicit `cwd` (e.g.
  `new ClaudeCodeAgent({ cwd: process.cwd() })`) OVERRIDES the worktree, so the agent then reads
  and writes the repo root instead of the isolated worktree. Its branch stays empty and a
  downstream merge/land step has nothing to merge. Leave agent `cwd` unset and let
  `<Worktree>` supply it. Likewise, helpers that touch a worktree's files must use
  `ctx.worktreePath(...)` or `ctx.resolveWorktreePath(...)`; never reconstruct paths with
  `process.cwd()`, `import.meta.dir`, `../..`, or fallback candidate lists.
- jj-backed worktrees include colocated git metadata, so git-native CLI agents such as Codex
  resolve the worktree directory as their repository root. Do not compensate by setting an
  agent `cwd`; that defeats the worktree isolation above.
- **Dependencies are not installed in a fresh worktree.** Agent tasks inside a `<Worktree>`
  get a `[smithers worktree isolation]` preamble prepended to their prompt: do all work inside
  the worktree, install dependencies fresh there (e.g. `pnpm install` at the worktree root),
  and never symlink node_modules from, or write into, the parent checkout. An install through
  a shared node_modules link rewrites the parent checkout's workspace links to point into the
  worktree, and every one of them dangles once the worktree is removed.
- **jj continuously snapshots a worktree's working copy onto its bookmark.** A compute
  `<Task>` (or helper) that detects changes with `git status --porcelain` can see a *clean*
  tree and skip its work, because jj has already committed the working-copy edits to the
  bookmark, and `git checkout` or `git restore` of a file in the worktree won't stick for the
  same reason (jj re-materializes it). Detect a worktree's changes by diffing against its
  base branch (`jj diff --from <baseBranch> --to @`, or `git diff <baseBranch>...HEAD`), not
  by git's dirty-state, and let the merge/land step operate on the committed bookmark rather
  than re-running `git add`/`git commit`.
- Innermost `<Worktree>` wins when nested; duplicate ids are rejected.
- Empty/whitespace `path` is rejected at render time.
- A relative `path` resolves against the **launch root**, not the workflow file's directory. The launch root is `--root` (if given), else the nearest ancestor of the operator working directory that contains a `.smithers/` package, else the operator working directory. It is never `dirname(workflowFile)`; `workflowPath` is threaded through render separately and is not used as the path base. A relative `path` like `wt-a` lands beside wherever you ran `bunx smithers-orchestrator up <path>` from, which can differ from where the workflow source lives. Smithers emits a one-time warning for relative worktree paths showing the base and resolved absolute path. Pass an absolute `path` (for example `/tmp/smithers/wt-a`) when you need a deterministic location, and read back the resolved location with `ctx.worktreePath(...)` or `ctx.resolveWorktreePath(...)` rather than reconstructing it.

---

## <ReviewLoop>

> Produce, review, and fix in a loop until the reviewer approves.

```ts
// Props
import { ReviewLoop } from "smithers-orchestrator";

type ReviewLoopProps = {
  id?: string; // default "review-loop"; task ids derived as {id}-produce, {id}-review
  producer: AgentLike;
  reviewer: AgentLike | AgentLike[];
  produceOutput: OutputTarget;
  reviewOutput: OutputTarget; // must include `approved: boolean`
  maxIterations?: number; // default 5
  onMaxReached?: "return-last" | "fail"; // default "return-last"
  skipIf?: boolean;
  children: string | ReactNode; // initial producer prompt
};
```

```tsx
export default smithers(() => (
  <Workflow name="code-review">
    <ReviewLoop
      producer={coder}
      reviewer={reviewer}
      produceOutput={outputs.code}
      reviewOutput={outputs.review}
      maxIterations={3}
    >
      Implement a REST API for user authentication with JWT tokens.
    </ReviewLoop>
  </Workflow>
));
```

## Notes

- The runtime reads `approved` each frame to decide whether to loop.
- On subsequent iterations the producer is re-run, but the loop does not wire reviewer output into the producer via a `needs` dependency. Any feedback the producer sees comes from the runtime/agent conversation history carrying prior outputs, not from the component's declared data flow.

---

## <Optimizer>

> Generate, evaluate, and improve in a loop until a target score is reached.

```ts
// Props
import { Optimizer } from "smithers-orchestrator";

type OptimizerProps = {
  id?: string; // default "optimizer"; task ids {id}-generate, {id}-evaluate
  generator: AgentLike;
  evaluator: AgentLike | ((candidate: unknown) => unknown | Promise<unknown>); // function = compute task
  generateOutput: OutputTarget;
  evaluateOutput: OutputTarget; // must include `score: number`
  targetScore?: number; // omit to run all iterations
  maxIterations?: number; // default 10
  onMaxReached?: "return-last" | "fail"; // default "return-last"
  skipIf?: boolean;
  children: string | ReactNode; // initial generation prompt
};
```

```tsx
export default smithers(() => (
  <Workflow name="prompt-optimizer">
    <Optimizer
      generator={promptEngineer}
      evaluator={evaluator}
      generateOutput={outputs.prompt}
      evaluateOutput={outputs.evaluation}
      targetScore={90}
      maxIterations={5}
    >
      Generate a prompt for summarizing legal documents.
    </Optimizer>
  </Workflow>
));
```

## Notes

- `score` drives convergence against `targetScore`.
- A function `evaluator` renders as a compute task rather than an agent task.

---

## <ContentPipeline>

> Typed waterfall of refinement stages, each depending on the previous.

```ts
// Props
import { ContentPipeline } from "smithers-orchestrator";

type ContentPipelineProps = {
  id?: string;
  stages: ContentPipelineStage[];
  skipIf?: boolean;
  children: string | ReactNode; // initial prompt for stage[0]
};

type ContentPipelineStage = {
  id: string;
  agent: AgentLike;
  output: OutputTarget;
  label?: string;
};
```

```tsx
export default smithers(() => (
  <Workflow name="blog-pipeline">
    <ContentPipeline
      stages={[
        { id: "outline", agent: outliner, output: outputs.outline, label: "Create outline" },
        { id: "draft", agent: writer, output: outputs.draft, label: "Write draft" },
        { id: "edit", agent: editor, output: outputs.edited, label: "Edit and polish" },
      ]}
    >
      Write a blog post about building AI workflows with React components.
    </ContentPipeline>
  </Workflow>
));
```

## Notes

- Each stage after the first depends on the previous via `needs`.
- Stage `id` values must be unique within the workflow.

---

## <Sidecar>

> Run a cheap shadow agent beside a primary task and compare their scorer results.

```ts
// Props
import { Sidecar, computeSidecarDelta } from "smithers-orchestrator";

type SidecarProps = {
  id?: string;                         // default: "sidecar"
  agent: AgentLike;                    // primary agent
  sidecar: AgentLike;                  // cheap shadow agent
  output: OutputTarget;                // primary output target
  sidecarOutput?: OutputTarget;        // default: output
  scorers?: ScorersMap;                // attached to both tasks
  prompt?: string | ReactNode;
  input?: string | ReactNode;
  maxConcurrency?: number;
  groundTruth?: unknown;
  context?: unknown;
  primaryLabel?: string;
  sidecarLabel?: string;
  skipIf?: boolean;
  children?: string | ReactNode;
};

type SidecarDelta = {
  primaryScore: number | null;
  sidecarScore: number | null;
  delta: number | null;
  cheaperWins: boolean;
};
```

```tsx
<Workflow name="cheap-model-shadow">
  <Sidecar
    id="answer"
    agent={primaryAgent}
    sidecar={cheapAgent}
    output={outputs.answer}
    scorers={scorers}
  >
    Answer the customer question with the most accurate response.
  </Sidecar>
</Workflow>
```

## Behavior

`<Sidecar>` renders a `<Parallel>` with two `<Task>` children. The primary task uses the component `id`, writes to `output`, and is the node downstream workflow code should consume. The sidecar task uses `${id}-sidecar`, receives the same prompt, gets `continueOnFail: true`, and writes to `sidecarOutput` when supplied or to `output` by default.

Both tasks receive the same `scorers` prop. Smithers records live scorer results in `_smithers_scorers` once per task node id, so `bunx smithers-orchestrator scores RUN_ID` can show the primary row and the sidecar row separately.

Use `computeSidecarDelta(rows, { primaryNodeId, sidecarNodeId, scorerId })` after reading persisted scorer rows. It derives `primaryScore`, `sidecarScore`, `delta`, and `cheaperWins` from those rows only.

## Notes

- The sidecar can fail without failing the workflow.
- The sidecar result does not replace the primary result.
- `delta` is `primaryScore - sidecarScore`.
- `cheaperWins` is true when the sidecar score is greater than or equal to the primary score.

---

## <DriftDetector>

> Capture state, compare against a baseline, and alert on meaningful drift.

```ts
// Props
import { DriftDetector } from "smithers-orchestrator";

type DriftDetectorProps = {
  id?: string; // default "drift"; ids {id}-capture, {id}-compare
  captureAgent: AgentLike;
  compareAgent: AgentLike;
  captureOutput: OutputTarget;
  compareOutput: OutputTarget; // include `drifted: boolean`
  baseline: unknown;
  alertIf?: (comparison: any) => boolean; // default: comparison.drifted === true
  alert?: ReactElement;
  poll?: { intervalMs?: number; maxPolls?: number }; // default maxPolls = 100; intervalMs is reserved, not yet passed to the Loop
  skipIf?: boolean;
};
```

```tsx
<Workflow name="api-drift-check">
  <DriftDetector
    captureAgent={snapshotAgent}
    compareAgent={diffAgent}
    captureOutput={outputs.capture}
    compareOutput={outputs.compare}
    baseline={{ endpoints: ["/users", "/orders"], schemaHash: "abc123" }}
    alert={
      <Task id="notify" output={outputs.notify} agent={slackAgent}>
        API drift detected. Notify the team.
      </Task>
    }
  />
</Workflow>
```

## Notes

- Without `poll`, the component runs once; with `poll`, it wraps in a Loop.
- When comparison output exists, `alertIf` decides whether to render `alert`; without `alertIf`, `comparison.drifted === true` is the trigger.
- Without `alert`, the component compares but takes no action on drift.

---

## <ScanFixVerify>

> Scan for problems, fix in parallel, verify, then report in a retry loop.

```ts
// Props
import { ScanFixVerify } from "smithers-orchestrator";

type ScanFixVerifyProps = {
  id?: string; // default "sfv"
  scanner: AgentLike;
  fixer: AgentLike | AgentLike[]; // array cycles across issues
  verifier: AgentLike;
  scanOutput: OutputTarget; // include `issues: Array`
  fixOutput: OutputTarget;
  verifyOutput: OutputTarget;
  reportOutput: OutputTarget;
  maxConcurrency?: number; // omit for no per-group cap (bounded only by the run-level concurrency limit, default 4)
  maxRetries?: number; // default 3
  skipIf?: boolean;
  children?: ReactNode; // scan prompt
};
```

```tsx
<Workflow name="lint-fix">
  <ScanFixVerify
    scanner={lintAgent}
    fixer={fixerAgent}
    verifier={verifyAgent}
    scanOutput={outputs.scan}
    fixOutput={outputs.fix}
    verifyOutput={outputs.verify}
    reportOutput={outputs.report}
    maxRetries={5}
    maxConcurrency={3}
  >
    Scan the codebase for linting errors and type issues.
  </ScanFixVerify>
</Workflow>
```

## Notes

- The loop runs up to `maxRetries` scan-fix-verify cycles. Early exit based on verifier output is not currently wired; the loop always completes all iterations (then ends via `onMaxReached: return-last`).
- The report task always runs, even when retries are exhausted.

---

## <Poller>

> Poll an external condition with configurable backoff until satisfied or timed out.

```ts
// Props
import { Poller } from "smithers-orchestrator";

type PollerProps = {
  id?: string; // default "poll"
  check: AgentLike | (() => Promise<unknown> | unknown);
  checkOutput: OutputTarget; // must include `satisfied: boolean`
  maxAttempts?: number; // default 30
  backoff?: "fixed" | "linear" | "exponential"; // default "fixed"
  intervalMs?: number; // delay BETWEEN attempts, default 5000
  checkTimeoutMs?: number; // timeout for one check attempt; unbounded when unset
  onTimeout?: "fail" | "return-last"; // default "fail"
  skipIf?: boolean;
  children?: ReactNode; // condition description
};
```

```tsx
<Workflow name="wait-for-deploy">
  <Poller
    check={statusChecker}
    checkOutput={outputs.check}
    maxAttempts={20}
    intervalMs={10_000}
    backoff="exponential"
    onTimeout="fail"
  >
    Check whether the deployment to production has completed successfully.
  </Poller>
</Workflow>
```

## Notes

- `satisfied` drives the loop's `until`.
- The first attempt runs immediately; `intervalMs` is the delay *between* attempts.
- Backoff scales that gap. For the Nth gap (1-indexed): fixed = `intervalMs`;
  linear = `intervalMs * N`; exponential = `intervalMs * 2^(N-1)`.
- The gap is a durable `<Timer>`: the run parks as
  `waiting-timer` between attempts and survives a crash or resume, rather than
  sleeping in-process. A detached run is resumed by the gateway's timer sweep.
- `intervalMs` does **not** bound how long a check may run. Use `checkTimeoutMs`
  to cap a single attempt.

---

## <Runbook>

> Sequential steps with risk classification; safe auto-runs, risky/critical gate on approval.

```ts
// Props
import { Runbook } from "smithers-orchestrator";

type RunbookProps = {
  id?: string; // used as step-id prefix; defaults to "runbook" when omitted
  steps: RunbookStep[];
  defaultAgent?: AgentLike;
  stepOutput: OutputTarget;
  approvalRequest?: Partial<ApprovalRequest>;
  onDeny?: "fail" | "skip"; // default "fail"
  skipIf?: boolean;
};

type RunbookStep = {
  id: string;
  agent?: AgentLike;
  command?: string;
  risk: "safe" | "risky" | "critical"; // critical adds `elevated: true` to approval meta
  label?: string;
  output?: OutputTarget;
};
```

```tsx
export default smithers(() => (
  <Workflow name="deploy-runbook">
    <Runbook
      defaultAgent={ops}
      stepOutput={outputs.stepResult}
      steps={[
        { id: "health-check", command: "curl -f https://api.example.com/health", risk: "safe" },
        { id: "backup-db", command: "pg_dump prod > backup.sql", risk: "risky" },
        { id: "run-migration", command: "npx prisma migrate deploy", risk: "critical" },
        { id: "smoke-test", command: "npm run test:smoke", risk: "safe" },
      ]}
    />
  </Workflow>
));
```

## Notes

- Each step depends on the previous via `needs`; execution order is guaranteed.
- Critical steps set `elevated: true` in approval metadata for stronger auth UIs.
- Approval output is stored at `{prefix}-{step.id}-approval-decision`.

---

## <Supervisor>

> Boss plans, workers run in parallel, boss reviews and re-delegates failures.

```ts
// Props
import { Supervisor } from "smithers-orchestrator";

type SupervisorProps = {
  id?: string;                                  // default: "supervisor"
  boss: AgentLike;
  workers: Record<string, AgentLike>;           // { coder, tester, ... }
  planOutput: OutputTarget;                     // { tasks: [{ id, workerType, instructions }] }
  workerOutput: OutputTarget;
  reviewOutput: OutputTarget;                   // { allDone: boolean, retriable: string[] }
  finalOutput: OutputTarget;
  maxIterations?: number;                       // default: 3
  maxConcurrency?: number;                      // default: 5
  useWorktrees?: boolean;                       // default: false
  skipIf?: boolean;
  children: string | ReactNode;                 // goal/prompt for the boss
};
```

```tsx
export default smithers(() => (
  <Workflow name="build-feature">
    <Supervisor
      boss={boss}
      workers={{ coder, tester }}
      planOutput={outputs.plan}
      workerOutput={outputs.workerResult}
      reviewOutput={outputs.review}
      finalOutput={outputs.final}
      maxIterations={3}
      maxConcurrency={4}
    >
      Build the user authentication module with tests.
    </Supervisor>
  </Workflow>
));
```

## Notes

- Generated node ids: `{id}-plan`, `{id}-loop`, `{id}-worker-{type}`, `{id}-review`, `{id}-final`.
- Workers run with `continueOnFail`; a single failure does not abort the cycle.
- With `useWorktrees`, each worker runs in `.worktrees/{prefix}-worker-{type}` on branch `worker/{prefix}-worker-{type}`.

---

## <MergeQueue>

> Queue child tasks so at most maxConcurrency run; defaults to single-lane.

```ts
// Props
import { MergeQueue } from "smithers-orchestrator";

type MergeQueueProps = {
  id?: string;
  maxConcurrency?: number;  // default: 1
  skipIf?: boolean;
  children?: ReactNode;
};
```

```tsx
<MergeQueue maxConcurrency={2}>
  {items.map((it, i) => (
    <Task key={i} id={`t${i}`} output={outputs.outputC}>{{ value: i }}</Task>
  ))}
</MergeQueue>
```

## Notes

- Innermost group determines the effective cap for its descendants.
- Tasks outside the queue are unaffected by its limit.

---

## <CheckSuite>

> Parallel checks with auto-aggregated pass/fail verdict.

```ts
// Props
import { CheckSuite } from "smithers-orchestrator";

type CheckConfig = { id: string; agent?: AgentLike; command?: string; label?: string };

type CheckSuiteProps = {
  id?: string;                                              // default: "checksuite"
  checks: CheckConfig[] | Record<string, Omit<CheckConfig, "id">>;
  verdictOutput: OutputTarget;
  strategy?: "all-pass" | "majority" | "any-pass";          // default: "all-pass"
  maxConcurrency?: number;                                  // default: Infinity
  continueOnFail?: boolean;                                 // default: true
  skipIf?: boolean;
};
```

```tsx
<Workflow name="ci-checks">
  <CheckSuite
    checks={[
      { id: "lint", agent: lintAgent, label: "ESLint" },
      { id: "typecheck", agent: typecheckAgent, label: "TypeScript" },
      { id: "test", agent: testAgent, label: "Unit Tests" },
    ]}
    verdictOutput={outputs.verdict}
    strategy="all-pass"
  />
</Workflow>
```

## Notes

- Check task ids are `{prefix}-{checkId}`; verdict is `{prefix}-verdict`.
- `strategy` is evaluated in pure code: `all-pass` requires every check to pass, `majority` requires more than half (`passCount*2 > total`), and `any-pass` requires at least one to pass.
- Use `command` instead of `agent` for shell-based checks.

---

## <ClassifyAndRoute>

> Classify items into categories, then route each to a category-specific agent in parallel.

```ts
// Props
import { ClassifyAndRoute } from "smithers-orchestrator";

type CategoryConfig = {
  agent: AgentLike;
  output?: OutputTarget;
  prompt?: (item: unknown) => string;
};

type ClassifyAndRouteProps = {
  id?: string;                                                       // prefix for auto-generated child task IDs; defaults to "classify-and-route"
  items: unknown | unknown[];
  categories: Record<string, AgentLike | CategoryConfig>;
  classifierAgent: AgentLike;
  classifierOutput: OutputTarget;
  routeOutput: OutputTarget;
  classificationResult?: { classifications: Array<{ category: string; itemId?: string }> } | null;
  maxConcurrency?: number;                                           // optional; unbounded when omitted
  skipIf?: boolean;
  children?: ReactNode;                                              // custom classifier prompt
};
```

```tsx
const classification = ctx.outputMaybe(outputs.classification, {
  nodeId: "classify-and-route-classify",
});

<Workflow name="support-router">
  <ClassifyAndRoute
    items={ctx.input.tickets}
    categories={{ billing: billingAgent, support: supportAgent, sales: salesAgent }}
    classifierAgent={classifierAgent}
    classifierOutput={outputs.classification}
    routeOutput={outputs.handled}
    classificationResult={classification}
  />
</Workflow>;
```

## Notes

- Two-phase: first render classifies; pass the result back via `classificationResult` to mount route handlers.
- Each entry's `category` must match a key in `categories`; unknown categories are silently skipped.
- Route tasks default to `continueOnFail`.

---

## <GatherAndSynthesize>

> Parallel gather from multiple sources, then synthesize into a unified result.

```ts
// Props
import { GatherAndSynthesize } from "smithers-orchestrator";

type SourceDef = {
  agent: AgentLike;
  prompt?: string;        // optional; defaults to a generated gather prompt
  output?: OutputTarget;
  children?: ReactNode;        // overrides prompt
};

type GatherAndSynthesizeProps = {
  id?: string;                                       // default: "gather-and-synthesize"
  sources: Record<string, SourceDef>;
  synthesizer: AgentLike;
  gatherOutput: OutputTarget;
  synthesisOutput: OutputTarget;
  gatheredResults?: Record<string, unknown> | null;  // typically from ctx.outputMaybe()
  maxConcurrency?: number;                           // default: Infinity
  synthesisPrompt?: string;
  skipIf?: boolean;
  children?: ReactNode;                              // overrides synthesisPrompt
};
```

```tsx
<Workflow name="research">
  <GatherAndSynthesize
    sources={{
      docs: { agent: docsAgent, prompt: "Search the documentation." },
      code: { agent: codeAgent, prompt: "Analyze the codebase." },
      issues: { agent: issueAgent, prompt: "Review open issues." },
    }}
    synthesizer={synthesisAgent}
    gatherOutput={outputs.gathered}
    synthesisOutput={outputs.synthesis}
    gatheredResults={gathered}
  />
</Workflow>
```

## Notes

- Synthesis task auto-receives `needs` for every source, gating it on all gathers.
- Source `children` takes priority over `prompt`.
- When `gatheredResults` is provided, it is folded into the default synthesis prompt.

---

## <Panel>

> Parallel specialist agents review the same input; a moderator synthesizes results.

```ts
// Props
import { Panel } from "smithers-orchestrator";

// agent may be a single agent or a failover chain (AgentLike[]) run as one panelist.
type PanelistConfig = { agent: AgentLike | AgentLike[]; role?: string; label?: string };

type PanelTaskOptions = { continueOnFail?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number };

type PanelProps = {
  id?: string;                                           // default: "panel"
  // each entry: an agent, a PanelistConfig, or a failover chain (AgentLike[])
  panelists: Array<PanelistConfig | AgentLike | AgentLike[]>;
  moderator: AgentLike | AgentLike[];                    // a chain runs as failover
  panelistOutput: OutputTarget;
  moderatorOutput: OutputTarget;
  strategy?: "synthesize" | "vote" | "consensus";        // default: "synthesize"
  minAgree?: number;                                     // for "vote" / "consensus"
  maxConcurrency?: number;                               // default: Infinity
  panelistTaskProps?: PanelTaskOptions;                  // extra Task props for each panelist
  moderatorTaskProps?: PanelTaskOptions;                 // extra Task props for the moderator
  skipIf?: boolean;
  children: string | ReactNode;                          // prompt sent to every panelist
};
```

```tsx
<Workflow name="code-review-panel">
  <Panel
    panelists={[
      { agent: securityAgent, role: "Security Reviewer" },
      { agent: qualityAgent, role: "Code Quality Reviewer" },
      { agent: architectureAgent, role: "Architecture Reviewer" },
    ]}
    moderator={moderatorAgent}
    panelistOutput={outputs.review}
    moderatorOutput={outputs.synthesis}
  >
    Review the changes in src/auth/ for security, quality, and architecture concerns.
  </Panel>
</Workflow>
```

## Notes

- Panelist task ids: `{prefix}-{label|role|panelist-N}`; moderator is `{prefix}-moderator`.
- `strategy` and `minAgree` are passed as prompt context to the moderator, which interprets them.
- All panelists write to the same `panelistOutput` schema, differentiated by task id.

---

## <Debate>

> Adversarial multi-round debate between proposer and opponent, then judge verdict.

```ts
// Props
import { Debate } from "smithers-orchestrator";

type DebateProps = {
  id?: string;                       // default: "debate"
  proposer: AgentLike;               // arguing FOR
  opponent: AgentLike;               // arguing AGAINST
  judge: AgentLike;                  // renders final verdict
  rounds?: number;                   // default: 2
  argumentOutput: OutputTarget;
  verdictOutput: OutputTarget;
  topic: string | ReactNode;
  skipIf?: boolean;
};
```

```tsx
<Workflow name="architecture-debate">
  <Debate
    proposer={monolithAdvocate}
    opponent={microservicesAdvocate}
    judge={architectureJudge}
    rounds={3}
    argumentOutput={outputs.argument}
    verdictOutput={outputs.verdict}
    topic="Should we migrate from a monolith to microservices for the payments system?"
  />
</Workflow>
```

## Notes

- Task ids: `{prefix}-proposer`, `{prefix}-opponent`, `{prefix}-judge`, loop `{prefix}-loop`.
- Loop runs exactly `rounds` iterations with `onMaxReached="return-last"`.
- Proposer and opponent share `argumentOutput`, differentiated by task id.

---

## <Kanban>

> Process items through ordered columns with a pluggable ticket source.

```ts
// Props
import { Kanban } from "smithers-orchestrator";

type ColumnDef = {
  name: string;
  agent: AgentLike;
  output: OutputTarget;
  prompt?: (ctx: { item: unknown; column: string }) => string;
  task?: Omit<Partial<TaskProps>, "agent" | "children" | "id" | "key" | "output" | "smithersContext">;  // retries, timeoutMs, etc.
};

type KanbanProps = {
  id?: string;                                       // default: "kanban"
  columns: ColumnDef[];
  useTickets: () => Array<{ id: string; [key: string]: unknown }>;
  agents?: Record<string, AgentLike>;                // overrides column-level agents
  maxConcurrency?: number;                           // default: unlimited (no cap), per column
  onComplete?: OutputTarget;
  until?: boolean;                                   // default: false
  maxIterations?: number;                            // default: 5
  skipIf?: boolean;
  children?: ReactNode | Record<string, unknown>;    // content for onComplete task
};
```

```tsx
const columns = [
  { name: "triage", agent: triageAgent, output: outputs.triage },
  { name: "work", agent: workerAgent, output: outputs.work },
  { name: "review", agent: reviewAgent, output: outputs.review },
];

<Workflow name="ticket-board">
  <Kanban
    columns={columns}
    useTickets={() => tickets}
    until={allDone}
    maxIterations={3}
  />
</Workflow>;
```

## Notes

- Item tasks default to `continueOnFail={true}`; use `column.task` to add retries or override.
- `useTickets` is called at render time; return different items per iteration for dynamic sources.
- Use `until` with `ctx.outputMaybe()` to exit when all items reach the final column.

---

## <DelegationChain>

> Recursive tiered delegation with risk probes, per-node backpressure, live edits, budgets, and scoring.

`<DelegationChain>` is the composite behind the
`delegation-chain` workflow: a `<Sequence>` of
seven phases running in `<Parallel>` with a live-edit signal listener. Every
phase is reactive: it re-derives its slice of the delegation tree from the
`dc*` output rows on each render, so fan-out materializes level by level as
rows land, and user edits or probe findings replan the affected subtree
without restarting anything.

```ts
// Props
import { DelegationChain } from "smithers-orchestrator";

type Tier = "fable" | "opus" | "sonnet" | "haiku";
type DelegationAgents = Partial<Record<Tier, AgentLike | AgentLike[]>>;

type DelegationSharedProps = {
  idPrefix?: string;              // physical node-id prefix; default "dc"
  agents: DelegationAgents;       // one agent (or failover chain) per tier label
  outputs: DelegationOutputs;     // the dc* output targets (see below)
  approvalPolicy?: string;        // absent = no approval gates anywhere
  tierOrder?: Tier[];             // strongest first; default ["fable","opus","sonnet","haiku"]
  maxDepth?: number;              // default 3
  maxConcurrency?: number;        // default 4
  maxDeriskRounds?: number;       // default 3
  poll?: boolean;                 // default true
  budget?: { maxUsd?: number; maxMinutes?: number };
  scorers?: { exec?: ScorersMap; review?: ScorersMap; run?: ScorersMap };
  skipIf?: boolean;
};

type DelegationChainProps = DelegationSharedProps & {
  prompt: string;                 // the original (possibly ambiguous) ask
  maxQuestions?: number;          // default 10
  prefetchDepth?: number;         // question forms rendered ahead; default 10
  maxAttempts?: number;           // exec/review attempts per leaf; default 3
  maxEdits?: number;              // live edits accepted per run; default 25
};
```

```tsx
import { CodexAgent, DelegationChain, createSmithers, delegationSchemas } from "smithers-orchestrator";

const { Workflow, smithers, outputs } = createSmithers({ ...delegationSchemas });

// The legacy tier keys are labels, not provider or model ids. Point them at
// Codex tiers by strength; the seeded workflow uses generated pools instead.
const sol = new CodexAgent({ model: "gpt-5.6-sol" });
const terra = new CodexAgent({ model: "gpt-5.6-terra" });
const luna = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });
const lunaCheap = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });

export default smithers((ctx) => (
  <Workflow name="delegation-chain">
    <DelegationChain
      prompt={ctx.input?.prompt ?? ""}
      agents={{ fable: sol, opus: terra, sonnet: luna, haiku: lunaCheap }}
      outputs={outputs}
    />
  </Workflow>
));
```

Register `delegationSchemas` in `createSmithers` and pass the matching
`outputs` subset. Tiers are labels only; missing tiers fall back to the
nearest configured tier in `tierOrder`. When `budget.maxMinutes` is set the
whole chain is wrapped in `<Aspects>` with a wall-clock `latencySlo`; dollar
budgets are enforced by per-leaf guard tasks over rolled-up `dcExec` actuals
(hard error over the limit, warning row at 80%).

## Phase composites

Each phase is exported separately for standalone use and takes
`DelegationSharedProps` (plus the extras noted):

| Component | Phase | Extra props |
| --- | --- | --- |
| `<GoalRefinement>` | Question forecast, prefetched forms, one durable question at a time, refined-prompt approval | `prompt`, `maxQuestions`, `prefetchDepth` |
| `<DelegationPlanning>` | Recursive decomposition fan-out until the frontier is all leaves | `prompt?` (standalone root brief; otherwise waits for the approved goal) |
| `<DelegationPreview>` | Zero-backpressure expected-output previews, skippable via `dc-skip-preview` | none |
| `<BackpressurePlanning>` | Every node declares gates and dependencies before execution | none |
| `<DeriskLoop>` | Risk probes, findings to the nearest parent, replan/reaffirm cascades | none |
| `<DelegationExecution>` | Dependency-ordered leaf pipelines: exec, gates, approvals, developer previews, budget guards | `maxAttempts` |
| `<DelegationScoring>` | Run-level score digest plus the satisfaction poll | none |
| `<DelegationEditListener>` | Re-arming `dc-edit` signal wait feeding the derisk loop | `until?`, `maxEdits` |

## Output tables

`delegationSchemas` (from `smithers-orchestrator`) registers every table;
`DelegationOutputs` is the matching prop shape. `dcApproval` is only needed
with an `approvalPolicy`, `dcBudget` only with a `budget`, and `dcScore` only
with run-level `scorers`.

| Table | One row per |
| --- | --- |
| `dcGoal` | Refined goal (prompt, assumptions, questions asked) |
| `dcQuestion` | Rendered question form / resolved answer |
| `dcForecast` | The goal agent's upfront question batch (internal) |
| `dcGoalApproval` | The human's `{ approved, refinedPrompt }` decision |
| `dcPlan` | A delegating node's plan: children, risks, estimates; replans append superseding rows |
| `dcPreview` | A leaf's never-executed expected output |
| `dcGates` | A node's declared gates and logical dependencies |
| `dcProbe` | A probe's finding (`planImpact`: changes / confirms / none) |
| `dcReplan` | A replan decision (`invalidated` or `reaffirmed`) with its trigger |
| `dcExec` | One execution attempt, with best-effort `actual` usage and the measured `commitRange` |
| `dcReview` | A review/check gate verdict for one attempt |
| `dcDevPreview` | A developer-preview build (`builtOk` is required to pass) |
| `dcApproval` | An approval-gate decision |
| `dcEdit` | A live user edit (signal payload) |
| `dcSkip` | The skip-previews signal payload |
| `dcPoll` | The end-of-run poll answers |
| `dcBudget` | A budget-guard checkpoint (`ok` or `warn`) |
| `dcScore` | The run-level scoring digest |

## Physical node ids

Every task id follows `dc:<logicalId>:<phase>` (the `dc` prefix is
`idPrefix`). Logical ids are `/`-separated paths (`root/core/reducer`); in
physical ids the `/` is encoded as `:` because node ids only allow
`[a-zA-Z0-9:_-]`. Row `logicalId` fields keep the `/` form. Phases:

- Goal: `dc:goal:forecast`, `dc:goal:forms:question-<seq>`,
  `dc:goal:question-<seq>` (the durable human answer),
  `dc:goal:goal`, `dc:goal:approve`
- Planning: `dc:<logicalId>:plan`, replan versions `dc:<logicalId>:plan-<k>`
- Previews: `dc:<logicalId>:preview`
- Gates: `dc:<logicalId>:gates`
- Derisk: `dc:<logicalId>:probe-<n>`, `dc:<logicalId>:replan-<k>`
- Execution: `dc:<logicalId>:exec`, `dc:<logicalId>:review-<i>` (reviews then
  checks, in declared order), `dc:<logicalId>:approval-<i>`,
  `dc:<logicalId>:dev-preview` (then `dev-preview-2`, ...),
  `dc:<logicalId>:budget`
- Scoring: `dc:root:score`, `dc:root:poll`

## Signals

Two fixed durable signal names (exported as `DC_EDIT_SIGNAL` and
`DC_SKIP_PREVIEW_SIGNAL`):

- `dc-edit` with payload `{ editId, logicalId, editedOutput, note? }`. Each
  delivered edit writes a `dcEdit` row that the derisk loop treats exactly
  like a plan-changing probe finding.
- `dc-skip-preview` with payload `{ skipped: true }`. Once a `dcSkip` row
  exists, no further preview tasks mount.

## Gates

A `dcGates` row declares an ordered list of gates:

```ts
type Gate =
  | { method: "review"; tier: Tier; brief: string }
  | { method: "check"; command: string }
  | { method: "approval"; policyMatch: string }   // honored only under an approvalPolicy
  | { method: "preview"; kind: "app" | "terminal" | "api" | "throwaway-ui" | "slideshow"; brief: string };
```

Review gates receive the node's output and its `dcExec.commitRange` values
with instructions to inspect the commits themselves; chunk-level reviews get
the union of their subtree's ranges (exec agents are wrapped with the
exported `withCommitRange`, which measures the working-copy commit before and
after the attempt, jj first with a git fallback). `preview` gates build a
developer preview after execution; `builtOk: false` fails the attempt like a
failed review. The gates prompt requires the root node to declare a
`slideshow` preview, so a run always ends showable.

## UI side

`smithers-orchestrator/gateway-react` exports the matching read model:
`useDelegationChain` folds a
run's `dc*` rows into a `DelegationGraph` (nodes with status, versions,
attention rollups, budget rollup, phase) and returns the `submitEdit`,
`skipPreviews`, `answerHuman`, and `submitPoll` actions. The pure reducer
`foldDelegation` is exported for tests and non-React use.

## Notes

- Approval gates require `outputs.dcApproval`; `<DelegationExecution>` throws
  `INVALID_INPUT` if the policy produced approval gates without it.
- `dcPlan.orchestration` (`"tasks" | "workflow"`) is reserved for planned
  higher-order orchestration (a node authoring its own workflow as its
  execution strategy); it is accepted and ignored today. The fold store behind
  `useDelegationChain` runs on Effect.ts behind the frozen hook signature.
- Scoring wiring: `scorers.exec` / `scorers.review` ride the exec and review
  tasks; `scorers.run` rides the digest task. The delegation scorers live in
  `smithers-orchestrator/scorers` (see the scorer reference).

---

## Recipes

> Tight, reusable patterns. Copy, paste, adapt.

Each recipe is a working snippet plus one line of context. They compose freely.

<Note>API reference: Reference overview maps every package to its exhaustive API page, each with links to source and tests.</Note>

If you want a ready-to-run outcome before writing workflow code, start with Starters or run `bunx smithers-orchestrator starters`.

## Default model routing

Smithers-generated workflows are Codex-first when usable Codex authentication
is available. GPT-5.6 Luna is now the broad default: start with
it for research, implementation, ordinary tool use, UI work, and routine-to-
substantial execution, then escalate only when the task earns the extra cost or
judgment. Pin the tier to the role:

| Work | Model |
|---|---|
| Research, implementation, ordinary tool use, UI, and routine-to-substantial execution | `gpt-5.6-luna` |
| Explicitly tool-heavy validation or higher-judgment escalation | `gpt-5.6-terra` |
| Planning, orchestration, final review, and the hardest reasoning | `gpt-5.6-sol` |

Luna remains the first implementation, research, and ordinary tool-use model
even for large or substantial changes; size alone does not promote it to Sol.
Escalate to Terra only for explicitly tool-heavy validation or higher-judgment
checking, and to Sol for
ambiguous architecture, final review, high-stakes decisions, or repeated Luna
failure. Non-Codex adapters are later sequential fallbacks when earlier Codex
agents are unavailable or fail; they are not parallel peers or a second opinion
on every run. `claude-fable-5` is the configured smart-role fallback or an
intentional Claude-native specialist. Explicit provider-specific workflows
continue to run as written. See the
SOTA model registry for the decision
rules and primary release links.

## Implement → review loop

Iterate until a reviewer signs off, with a hard cap.

```tsx
<Loop
  until={ctx.latest(outputs.review, "review")?.approved === true}
  maxIterations={5}
  onMaxReached="return-last"
>
  <Sequence>
    <Task id="implement" output={outputs.impl} agent={implementer}>
      {`${ctx.input.task}\nPrior review: ${ctx.latest(outputs.review, "review")?.feedback ?? "none"}`}
    </Task>
    <Task id="review" output={outputs.review} agent={reviewer}>
      {`Review the latest implementation. Return { approved, feedback }.`}
    </Task>
  </Sequence>
</Loop>
```

Stop conditions must be measurable (boolean, count, array length). Avoid "looks good" prompts; agents are literal.

In a `<Loop>` `until`, read the most recent iteration with `ctx.latest`. `ctx.outputMaybe(.., { nodeId })` without an explicit `iteration` resolves the *current render iteration* (which equals the loop iteration only for a single, non-nested loop, and is `0` when several loops coexist), so an `outputMaybe`-based `until` can silently never advance.

## Parallel Codex review

Two Codex tiers provide independent signals without routing to another provider.
Cost = the slower model's latency.

```tsx
<Parallel>
  <Task id={`${ticket.id}:review-sol`} output={outputs.review} agent={codexSol} continueOnFail>
    <ReviewPrompt reviewer="sol" />
  </Task>
  <Task id={`${ticket.id}:validate-terra`} output={outputs.review} agent={codexTerra} continueOnFail>
    <ReviewPrompt reviewer="terra" />
  </Task>
</Parallel>
```

`continueOnFail` keeps one tier's timeout from blocking the other.

## Approval gate with branching

Decision data drives the next branch.

```tsx
<Approval
  id="ship-decision"
  output={outputs.shipDecision}
  request={{ title: `Ship v${ctx.input.version}?`, summary: testReport }}
  onDeny="continue"
/>

{ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved
  ? <Task id="release" .../>
  : <Task id="rollback" .../>}
```

`onDeny`: `"fail"` aborts, `"continue"` proceeds without the gated branch, `"skip"` skips the gated tasks.

## Retry policy & timeouts

```tsx
<Task
  id="api-call"
  agent={agent}
  retries={3}
  retryPolicy={{ backoff: "exponential", initialDelayMs: 1000 }}
  timeoutMs={30_000}
>
  Call external API.
</Task>
```

Defaults to fit the work: simple tasks 30–60s + 1–2 retries, tool-heavy 2–5m + 1–2, large generations 5–10m + 0–1. Exponential backoff for rate-limited APIs.

## Optional, non-blocking step

```tsx
<Task id="lint" output={outputs.lint} agent={linter} continueOnFail>
  Run lint checks. Pipeline continues if this fails.
</Task>
```

Use for nice-to-have telemetry, lint, optional analysis.

## Conditional branch on output

```tsx
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });

{analysis?.risk === "high" ? (
  <Task id="escalate" output={outputs.escalation} agent={escalator}>
    {`Critical: ${analysis.summary}`}
  </Task>
) : null}
```

`ctx.outputMaybe` for control flow.

## Dynamic ticket discovery

Discover work, run each ticket, re-render to catch the next batch. Scales to large projects.

```tsx
export default smithers((ctx) => {
  const discover = ctx.latest(outputs.discover, "discover");
  const unfinished = (discover?.tickets ?? []).filter(
    (t) => !ctx.latest(outputs.report, `${t.id}:report`)
  );

  return (
    <Workflow name="big-project">
      <Sequence>
        <Branch if={unfinished.length === 0} then={<Discover />} />
        {unfinished.map((t) => (
          <TicketPipeline key={t.id} ticket={t} />
        ))}
      </Sequence>
    </Workflow>
  );
});
```

Use stable IDs (`t.id`, not array index) so resume matches.

## Coherent task with tools

One context boundary per logical operation, not per step. Splitting too finely loses cross-step reasoning.

```tsx
<Task id="fix-config-bugs" output={outputs.result} agent={agentWithTools}>
  {`Analyze config files in ${ctx.input.dir}, find bugs, fix them, write results.
   Use read, edit, bash. Return { summary, filesChanged }.`}
</Task>
```

## Per-agent least-privilege tools

```tsx
import { OpenAIAgent } from "smithers-orchestrator";

const researcher  = new OpenAIAgent({ model: "gpt-5.6-luna", instructions: "Return JSON" });
const validator   = new OpenAIAgent({ model: "gpt-5.6-terra", instructions: "...", tools: { read, grep } });
const reviewer    = new OpenAIAgent({ model: "gpt-5.6-sol", instructions: "...", tools: { read, grep } });
const implementer = new OpenAIAgent({ model: "gpt-5.6-luna", instructions: "...", tools: { read, write, edit, bash } });
```

Match the tool surface to the role.

## Side-effect tools with idempotency

External mutations must mark themselves and use the runtime idempotency key.

```tsx
import { defineTool } from "smithers-orchestrator/tools";

const createTicket = defineTool({
  name: "jira.create",
  schema: z.object({ title: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return jira.createIssue({ ...args, idempotencyKey: ctx.idempotencyKey });
  },
});
```

Retries reuse the same idempotency key, so a successful side effect from attempt 1 isn't doubled by attempt 2.

## Caching for iterative authoring

```tsx
<Workflow name="report" cache>
  <Task
    id="analyze"
    output={outputs.analysis}
    agent={analyst}
    cache={{ by: (ctx) => ({ repo: ctx.input.repo }), version: "v2" }}
  >
    {`Analyze ${ctx.input.repo}`}
  </Task>
  <Task id="report" output={outputs.report} agent={reporter} deps={{ analyze: outputs.analysis }}>
    {(deps) => `Report on ${deps.analyze.summary}`}
  </Task>
</Workflow>
```

Tweak the downstream Task without re-running the expensive upstream one. Don't cache side effects.

## Schemas in their own file

```ts
// schemas.ts
export const schemas = {
  analysis: z.object({ summary: z.string(), issues: z.array(z.string()) }),
  review:   z.object({ approved: z.boolean(), feedback: z.string() }),
  report:   z.object({ title: z.string(), body: z.string() }),
};

// workflow.tsx
import { schemas } from "./schemas";
const { Workflow, smithers, outputs } = createSmithers(schemas);
```

All data shapes in one place; new contributors read schemas.ts first.

## MDX prompt with auto-injected schema

```mdx
{/* Review.mdx */}
Review this code:

**Files**: {props.files.join(", ")}
**Tests**: {props.testsPassed}/{props.testsRun} passing

Return JSON matching schema:
{props.schema}
```

`props.schema` is the JSON-schema description of the Task's `outputSchema`, auto-injected. Keeps the prompt and the validator in sync.

In `.mdx` prompt files, prose that looks like JSX or HTML is parsed as JSX. Wrap examples such as `<Parallel maxConcurrency={N}>` in inline backticks or a fenced code block so the prompt module still compiles and exports its default prompt component.

## Custom hooks over `ctx`

```tsx
function useReviewState(ticketId: string) {
  const ctx = useCtx();
  const sol = ctx.latest("review", `${ticketId}:review-sol`);
  const terra = ctx.latest("review", `${ticketId}:validate-terra`);
  return { sol, terra, allApproved: !!(sol?.approved && terra?.approved) };
}
```

Workflow logic factors out into hooks the same way React UI logic does.

## VCS revert & per-attempt snapshots

Smithers records the current JJ change ID in `_smithers_attempts.jj_pointer` per attempt. Revert any attempt with a recorded JJ pointer to its exact workspace state:

```bash
bunx smithers-orchestrator revert workflow.tsx --run-id RUN_ID --node-id implement --attempt 1
```

Useful when an experiment leaves the worktree in a bad state.

## Time travel: fork, replay, diff

```bash
bunx smithers-orchestrator timeline RUN_ID --tree
bunx smithers-orchestrator diff RUN_ID NODE_ID
bunx smithers-orchestrator fork workflow.tsx --run-id RUN_ID --frame 5 --reset-node analyze --label exp1
bunx smithers-orchestrator replay workflow.tsx --run-id RUN_ID --frame 5 --restore-vcs
```

Fork makes a child run without starting it (add `--run` to start immediately); replay also makes a child run but immediately resumes it. `--restore-vcs` checks out the original revision so re-execution sees the same source.

## Scoring tasks

```tsx
import { schemaAdherenceScorer, latencyScorer, llmJudge } from "smithers-orchestrator/scorers";

<Task
  id="analyze"
  output={outputs.analysis}
  agent={analyst}
  scorers={{
    schema:  { scorer: schemaAdherenceScorer() },
    latency: { scorer: latencyScorer({ targetMs: 5000 }) },
    quality: {
      scorer: llmJudge({
        id: "analysis-quality",
        name: "Analysis Quality",
        description: "Rates whether the analysis is useful and well supported.",
        judge: analyst,
        instructions: "Return JSON with score from 0 to 1 and a short reason.",
        promptTemplate: ({ input, output }) =>
          `Rate the analysis quality.\nInput: ${JSON.stringify(input)}\nOutput: ${JSON.stringify(output)}`,
      }),
      sampling: { type: "ratio", rate: 0.1 },
    },
  }}
>
  Analyze...
</Task>
```

Scorers run after the task and never block. Sample expensive scorers with `ratio`.

## Eval suites for regressions

```jsonl
{"id":"happy-path","input":{"prompt":"Draft release notes"},"expected":{"status":"finished"}}
{"id":"quality-gate","input":{"prompt":"Find risky changes"},"expected":{"status":"finished","outputContains":{"analysis":[{"riskLevel":"low"}]}}}
```

```bash
bunx smithers-orchestrator eval workflow.tsx --cases evals/smoke.jsonl --suite smoke --force
```

Use eval suites when you need repeatable workflow-level checks. Assertions support `status`, `output` (exact match), and `outputContains` (partial match). Reports land in `.smithers/evals/<suite>.json`; the command exits non-zero on failures.

## Continue-as-new for very long runs

A run with too much accumulated state hands off to a fresh run with carried state.

```tsx
<ContinueAsNew when={iterationCount > 100} carry={{ summary: rolledUpState }} />
```

Avoids unbounded SQLite growth in long-lived loops.

## Hot reload while authoring

```bash
bunx smithers-orchestrator up workflow.tsx --hot
```

Edits to the workflow source apply on the next render frame without losing in-flight task state. Schema changes still require a fresh run.

## Fork agent session context

Every agent task produces a reusable session snapshot. `fork` starts a new task from a copy of another task's final context, without mutating the source.

```tsx
<Task id="investigate" agent={codexLuna} output={outputs.investigation}>
  Understand the bug and identify possible fixes.
</Task>

<Parallel>
  <Task id="minimal-fix" agent={codexLuna} fork="investigate" output={outputs.patch}>
    Try the minimal fix.
  </Task>
  <Task id="refactor-fix" agent={codexLuna} fork="investigate" output={outputs.patch}>
    Try the refactor fix.
  </Task>
</Parallel>
```

`fork` adds the source as a dependency (the forked task waits for it), copies its conversation into a fresh session, then submits the new prompt. Both branches above start from the same investigation and never affect each other. Chain it for follow-ups (`plan → implement → verify`); inside a `<Loop>` it forks the latest completed snapshot for that id. See `<Task>` fork.

## Read next

- How It Works: the model these recipes plug into.
- Components: full prop surface.
- CLI: every command.

---

## Common Footguns

> The handful of mistakes that bite people first, and the pattern that avoids each.

Smithers is durable by design, and most of its sharp edges trace back to one fact: a run is replayed from persisted state, not from memory. The mistakes below are the ones people hit first. Each links to the canonical reference for the full story.

## Resume and state

### Unstable task IDs break resume

The runtime keys completed work by task `id`. A changed id looks like a brand new task, and an id that disappears is dropped from the plan. Derive ids from data, never from a loop index or a timestamp.

```tsx
{tickets.map((t) => <TicketPipeline key={t.id} id={`${t.id}:work`} />)}
// NOT id={`work-${i}`} or id={`work-${Date.now()}`}
```

It is the same rule as React keys. See How It Works.

### Input is immutable after the first run

A run's `--input` is persisted when it starts. Resuming with different input is an error, not a silent override. If you need different input, start a new run.

### Code changes block resume, they do not merge

A workflow source change is a different workflow. Resume validates the source hash of the original run, so editing the file and then resuming is blocked. Start a new run instead. To change a workflow that is still running, use hot reload (`up --hot`): edits apply to newly scheduled tasks while in-flight tasks finish on their original code. See Recipes.

### `useState` is not durable

React state resets on every render, which here means every frame. Anything that must survive a crash belongs in a Task output read back through `ctx`, not in component state.

### Evidence files must be run-scoped or cleaned

If a workflow writes verdict files such as `artifacts/.../verify.json` and later reads them to decide whether work is done, include `ctx.runId` in the evidence path or delete the evidence directory at run start. A prior run's `verify.json` must never be able to satisfy a fresh run's done-check.

## Caching

### Do not cache side-effecting tasks

`cache` is for pure work that is expensive to recompute. Caching a deploy, an email, or a mutation means it silently does not run on a cache hit. The cache key is `cache.by(ctx)` plus `cache.version` plus the output schema signature, so a schema change invalidates the cache automatically and a stale cached row fails validation and misses safely. See How It Works.

## Side effects and retries

### Mark side-effecting tools and key them

Tasks retry, and a retried agent loop can call a tool again. A custom tool that writes to the world should declare `sideEffect: true` and pass `ctx.idempotencyKey` through to the downstream system so a retry is a no-op rather than a second charge. `ctx.idempotencyKey` is stable across retries and resumes for the same task iteration.

```ts
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";

const placeOrder = defineTool({
  name: "shop.place_order",
  description: "Place an order",
  schema: z.object({ sku: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return await shop.placeOrder({ sku: args.sku, idempotencyKey: ctx.idempotencyKey });
  },
});
```

### Decide in one task, act in another

Marking and keying the tool keeps a retry from double-charging. It does not make the
charge reviewable. An agent that both decides to send money and sends it cannot have
its decision inspected or rerun without risking a second charge. Have the deciding
task return a typed decision (`{ shouldPay, amount, reason }`), then put the actual
payout in its own downstream task behind an `<Approval>`. The
decision is reversible and replayable; the act is isolated, gated, and keyed. See
[Sequence for
reversibility](/guides/context-engineering#sequence-for-reversibility-isolate-the-irreversible)
for the full pattern.

## Tools and sandbox

### Agents get only the tools you grant

The five built-in tools (`read`, `write`, `edit`, `grep`, `bash`) are sandboxed to `rootDir`. Symlinks, network, and long-running calls are denied by default; `--allow-network` opens bash to the network. Grant least privilege per task: a reviewer gets `read` and `grep`, an implementer gets `write`, `edit`, and `bash`, and an agent with no `tools` cannot touch the filesystem at all. See How It Works.

## Autonomous and detached runs

### Agents need permission-bypass flags or a detached run hangs

A `ClaudeCodeAgent` or `CodexAgent` constructed with just a `model` will prompt
for permission before it edits files. In an interactive session you click
through; in a detached run (`up -d`) there is no one to click, so the task stalls
until its heartbeat timeout. For autonomous runs construct agents with the bypass
flags:

```tsx
const opus = new ClaudeCodeAgent({
  model: "claude-opus-4-8",
  permissionMode: "bypassPermissions",
  dangerouslySkipPermissions: true,
});
const codex = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  sandbox: "danger-full-access",
  dangerouslyBypassApprovalsAndSandbox: true,
  skipGitRepoCheck: true,
});
```

The named pools in `.smithers/agents.ts` are intentionally bypass-free for
interactive use, so do not reach for them in a detached workflow without adding
the flags.

### `--hot` is for a live process, not for resuming after edits

Hot reload (`up --hot`) applies workflow and prompt edits on the next render
frame of a process that is still running. It does not let you edit a workflow and
then `--resume` a previously suspended run (for example a detached run paused at
an approval gate): resume validates the source hash and rejects the changed file
with `RESUME_METADATA_MISMATCH`, with or without `--hot`. Changing task IDs or the
module graph always requires a fresh run. To iterate on a detached run that
suspends at gates, either keep a live `up --hot` process attached, or start a new
run and gate the already-finished phases off with an input flag so they skip.

### Never pin `cwd` on an agent you use inside `<Worktree>`

A pinned `cwd` takes precedence over the per-task root, so the agent reads and
writes the launch directory and commits to the base branch instead of its
worktree. Leave `cwd` unset and let `<Worktree>` (or the launch root) control the
directory. The engine logs a "pinned cwd overrides Worktree" warning when you get
this wrong.

## Time travel and VCS

### Revert and VCS-restoring replay change your working tree

These rewrite filesystem state, so treat them the way you would treat `git checkout` over uncommitted work.

- `revert` restores the workspace to a previous attempt's filesystem state and discards graph snapshots recorded after that attempt. It restores files only and lands them as a new change on top of the current working copy. See Revert to Attempt.
- `replay --restore-vcs` checks out the jj revision the snapshot was taken at, so re-execution sees the same source as the original run.

### `revert` requires jj

Smithers prefers `.jj` over `.git`. Pure Git repos run fine but cannot use `revert`, because there is no per-attempt change to restore. Install jj if you want attempt-level revert. See VCS.

### Worktree runs auto-rebase on resume

On resume of a worktree run, Smithers rebases onto the base branch (default `main`) and continues even if the rebase fails. Expect the branch to move.

### `<Worktree baseBranch>` must be a stable branch, not the current change

`baseBranch` defaults to `main` and should name a committed branch or a described commit. Do not pass the current working-copy commit (for example `jj log -r @` output): a jj `@` snapshots the launcher's uncommitted changes into an undescribed commit, so the worktree inherits that dirty tree and its branch ends up based on a commit jj refuses to push ("Won't push commit ... since it has no description"). Omit `baseBranch` to use `main`, or pass an explicit clean branch name.

## Outputs

### `ctx.outputMaybe` is undefined until the task runs

Reading a downstream output before its task has completed returns `undefined`, not a default. Guard it so a not-yet-run task does not crash the render.

```tsx
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
return analysis ? <Task id="report" output={outputs.report} agent={writer}>...</Task> : null;
```

When a task's only job is waiting on one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` children callback is the more direct form: it defers until the row exists and hands it straight to `children`, no guard variable needed.

### An output schema is a shared pool; do not write stray rows into one you filter

`ctx.outputs.<schema>` is every row written for that schema by every node and
every loop iteration. When you fan out and later read "the latest row for item X"
by filtering on a discriminator field, any other task that writes a row to the
same schema lands in that pool too. A setup or sentinel task that reuses a
filtered schema (for example writing a placeholder `validation` row from a
`prepare` step) can be picked up as if it were real per-item state. Give
unrelated streams their own schema, and always match on a discriminator you set
on every row.

```tsx
// prepare gets its OWN schema, not the validation schema the loop filters by deliverable
<Task id="prepare" output={outputs.prepare}>{() => ({ specFound: true })}</Task>
```

## Read next

- How It Works: the execution model these rules come from.
- Recipes: caching, hot reload, and VCS revert in context.

---

## Types

> Public TypeScript surface for smithers-orchestrator.

One source of truth: `tsc --emitDeclarationOnly` would produce something close to this. Import these types from `smithers-orchestrator` unless noted otherwise.

`createSmithers` is a **named export**:

```ts
import { createSmithers } from "smithers-orchestrator";
```

It returns a local API object for that workflow module. Destructure the pieces
you use (`Workflow`, `Task`, `Branch`, `Sandbox`, `smithers`, `outputs`) or keep
the object and use property access (`api.Workflow`, `api.Task`). Both styles are
equivalent for one factory; property access is clearer in files with multiple
factories, such as parent/child workflow examples.

There is no top-level default `smithers` export. The `smithers` wrapper function
is the property returned by `createSmithers(...)`, and
`smithers((ctx) => <Workflow ... />)` returns the `SmithersWorkflow` value that
workflow files usually export as their default export.

`input` is a reserved schema key on `createSmithers(...)`. It controls the
TypeScript type of `ctx.input`; every other key is an output schema and becomes
a typed output ref under `outputs.<key>`. Fresh runs validate the input row
against the generated input table before inserting it, but Smithers does **not**
call `inputSchema.parse()` and does not write Zod default values back into
`ctx.input`. Treat required fields in the Zod schema as the authored TypeScript
contract; for fields that may be omitted by a CLI/UI caller, use `.optional()` /
`.nullable()` and coalesce inside the workflow (`ctx.input.dryRun ?? false`) or
add an early compute task that performs stricter Zod parsing.

Prefer `output={outputs.someKey}` on tasks, approvals, sandboxes, and subflows.
The `outputs` object contains the exact Zod schema objects passed to
`createSmithers(...)`, so the task can infer `outputSchema`, validate the
returned row, persist it to the matching table, and feed the same schema to
native structured-output agents.

Major sections at a glance:
- **Workflow / Context**: `SmithersWorkflow`, `SmithersCtx`, `RunOptions`, `RunResult`, the entry points for defining and running workflows.
- **Task / Graph**: `TaskDescriptor`, `TaskProps`, `GraphSnapshot`, the shape of nodes at runtime and in the JSX layer.
- **Component props**: `WorkflowProps`, `ApprovalProps`, `SignalProps`, `LoopProps`, etc., all JSX component interfaces.
- **Errors**: `SmithersError`, `KnownSmithersErrorCode`, typed error codes; see Errors for descriptions.
- **Server / Gateway**: `ServerOptions`, `GatewayOptions`, `GatewayAuthConfig`, self-hosting configuration.
- **Scorers / Memory / OpenAPI / Observability**: sub-path imports (`smithers-orchestrator/scorers`, `/memory`, `/openapi`, `/observability`).

```ts
// =============================================================================
// Workflow
// =============================================================================

interface SmithersWorkflow<Schema = unknown> {
  readonly readableName?: string;
  readonly description?: string;
  readonly ui?: WorkflowViewDefinition;
  readonly tui?: WorkflowViewDefinition;
  readonly db?: unknown;
  readonly build: (ctx: SmithersCtx<Schema>) => JSX.Element;
  readonly opts: SmithersWorkflowOptions;
  readonly schemaRegistry?: Map<string, SchemaRegistryEntry>;
  readonly zodToKeyName?: Map<import("zod").ZodObject<import("zod").ZodRawShape>, string>;
}

type WorkflowViewDefinition = {
  kind: "ui" | "tui";
  title?: string;
  props?: Record<string, unknown>;
  entry?: string;
  path?: string;
  source?: string;
  exportName?: string;
  literal?: WorkflowLiteralViewNode;
};

type SmithersWorkflowOptions = {
  alertPolicy?: SmithersAlertPolicy;
  cache?: boolean;
  // Explicit workflow-level output schema/table used to populate
  // RunResult.output (and therefore a parent <Subflow>'s child result).
  // Defaults to the schema key literally named `output`.
  output?: SmithersWorkflowOutputTarget;
  workflowHash?: string;
};

// A Zod schema (createSmithers(...).outputs.<key>), a Drizzle table, or a
// string schema key. Restated in @smithers-orchestrator/scheduler, which must
// not depend on zod or components.
type SmithersWorkflowOutputTarget =
  | { readonly _def: unknown }
  | { readonly $inferSelect: Record<string, unknown> }
  | string;

type SchemaRegistryEntry = {
  table: any;
  zodSchema: import("zod").ZodObject<any>;
};

type SmithersAlertPolicy = {
  defaults?: SmithersAlertPolicyDefaults;
  rules?: Record<string, SmithersAlertPolicyRule>;
  reactions?: Record<string, SmithersAlertReaction>;
};

type SmithersAlertSeverity = "info" | "warning" | "critical";
type SmithersAlertLabels = Record<string, string>;
type SmithersAlertPolicyDefaults = {
  owner?: string;
  severity?: SmithersAlertSeverity;
  runbook?: string;
  labels?: SmithersAlertLabels;
};
type SmithersAlertPolicyRule = SmithersAlertPolicyDefaults & {
  afterMs?: number;
  reaction?: string | SmithersAlertReaction;
};
type SmithersAlertReaction =
  | { kind: "emit-only" }
  | { kind: "pause" }
  | { kind: "cancel" }
  | { kind: "open-approval" }
  | { kind: "deliver"; destination: string };

// =============================================================================
// Context
// =============================================================================

declare class SmithersCtx<Schema extends unknown = unknown> {
  runId: string;
  iteration: number;
  iterations: Record<string, number> | undefined;
  input: Schema extends { input: infer T } ? T : unknown;
  auth: RunAuthContext | null;
  outputs: OutputAccessor<Schema>;

  // table is the schema key or output target from createSmithers,
  // such as "review" or outputs.review, not the generated SQL table name.
  output(table: any, key: OutputKey): any;
  outputMaybe(table: any, key: OutputKey): any | undefined;
  latest(table: any, nodeId: string): any | undefined;
  latestArray(value: unknown, schema: SafeParser): unknown[];
  iterationCount(table: any, nodeId: string): number;
  resolveTableName(table: any): string;
  resolveRow(table: any, key: OutputKey): any | undefined;
}

type OutputKey = { nodeId: string; iteration?: number };
type SafeParser = {
  safeParse(value: unknown):
    | { success: true; data: unknown }
    | { success: false; error?: unknown };
};
type InferRow<TTable> = TTable extends { $inferSelect: infer R } ? R : never;
type InferOutputEntry<T> =
  T extends import("zod").ZodTypeAny ? import("zod").infer<T>
  : T extends { $inferSelect: any } ? InferRow<T>
  : never;
type FallbackTableName<Schema> = [keyof Schema & string] extends [never] ? string : never;
type OutputAccessor<Schema, TRow = unknown> = {
  (table: FallbackTableName<Schema>): Array<TRow>;
  <K extends keyof Schema & string>(table: K): Array<InferOutputEntry<Schema[K]>>;
} & {
  [K in keyof Schema & string]: Array<InferOutputEntry<Schema[K]>>;
};
type OutputSnapshot<TFallback = unknown> = {
  [tableName: string]: Array<TFallback>;
};

type RunAuthContext = {
  triggeredBy: string;
  scopes: string[];
  role: string;
  createdAt: string;
};

// =============================================================================
// Run
// =============================================================================

type RunOptions = {
  runId?: string;
  parentRunId?: string | null;
  input: Record<string, unknown>;
  maxConcurrency?: number;          // default 4
  requireRerenderOnOutputChange?: boolean;  // default true; re-render the frame on every task completion
  onProgress?: (e: SmithersEvent) => void;
  signal?: AbortSignal;
  pauseSignal?: AbortSignal;        // graceful pause: stop scheduling, let in-flight finish, park `paused`
  resume?: boolean;
  force?: boolean;                  // resume even if marked running
  workflowPath?: string;
  rootDir?: string;
  logDir?: string | null;
  allowNetwork?: boolean;           // default false; bash tool network access
  maxOutputBytes?: number;          // default 200000
  toolTimeoutMs?: number;           // default 60000
  hot?: boolean | HotReloadOptions;
  annotations?: Record<string, string | number | boolean>;
  auth?: RunAuthContext | null;
  config?: Record<string, unknown>;
  effectPlatformRuntime?: "bun" | "node" | "worker";     // swappable @effect/platform layer; "node"/"worker" require effectPlatformLayer
  effectPlatformLayer?: Layer.Layer<any, never, never>;  // e.g. NodeContext.layer from a Node serverless entrypoint
  cliAgentToolsDefault?: "all" | "explicit-only";  // default "all"
  initialOutputs?: OutputSnapshot;  // seed prior outputs (resume/fork)
  initialIteration?: number;        // seed the starting loop iteration
  initialIterations?: Record<string, number> | ReadonlyMap<string, number>;  // per-loop iteration seeds
  resumeClaim?: {                   // internal supervisor coordination
    claimOwnerId: string;
    claimHeartbeatAtMs: number;
    restoreRuntimeOwnerId?: string | null;
    restoreHeartbeatAtMs?: number | null;
  };
};

type HotReloadOptions = {
  rootDir?: string;
  outDir?: string;                  // default .smithers/hmr under rootDir
  maxGenerations?: number;          // default 3
  cancelUnmounted?: boolean;        // default false
  debounceMs?: number;              // default 100
};

type RunResult = {
  readonly runId: string;
  readonly status: RunStatus;
  readonly output?: unknown;
  readonly error?: unknown;
  readonly nextRunId?: string;      // set when the run continued-as-new
};

type RunStatus =
  | "running"
  | "waiting-approval"
  | "waiting-event"
  | "waiting-timer"
  | "waiting-quota"
  | "finished"
  | "continued"
  | "failed"
  | "cancelled";

type RetryTaskOptions = {
  runId: string;
  nodeId: string;
  iteration?: number;
  resetDependents?: boolean;        // default true
  force?: boolean;                  // default false
  onProgress?: (e: SmithersEvent) => void;
};

type RetryTaskResult = {
  success: boolean;
  resetNodes: string[];
  error?: string;
};

// =============================================================================
// Task
// =============================================================================

type TaskDescriptor = {
  nodeId: string;
  ordinal: number;
  iteration: number;
  ralphId?: string;
  dependsOn?: string[];
  needs?: Record<string, string>;
  forkSource?: string;              // logical id of the task whose session this task forks
  worktreeId?: string;
  worktreePath?: string;
  worktreeBranch?: string;
  worktreeBaseBranch?: string;
  outputTable: unknown | null;
  outputTableName: string;
  outputRef?: import("zod").ZodObject<any>;
  outputSchema?: import("zod").ZodObject<any>;
  parallelGroupId?: string;
  parallelMaxConcurrency?: number;
  subtreeGroupId?: string;          // nearest ancestor <Parallel subtreeConcurrency> group
  subtreeChildKey?: string;         // direct child of that parallel this task descends from
  subtreeMax?: number;              // its cap on in-flight direct children
  needsApproval: boolean;
  waitAsync?: boolean;
  approvalMode?: "gate" | "decision" | "select" | "rank";
  approvalOnDeny?: "fail" | "continue" | "skip";
  approvalOptions?: ApprovalOption[];
  approvalAllowedScopes?: string[];
  approvalAllowedUsers?: string[];
  approvalAutoApprove?: {
    after?: number;
    audit?: boolean;
    conditionMet?: boolean;
    revertOnMet?: boolean;
  };
  skipIf: boolean;
  retries: number;
  retryPolicy?: RetryPolicy;
  timeoutMs: number | null;
  heartbeatTimeoutMs: number | null;
  continueOnFail: boolean;
  cachePolicy?: CachePolicy;
  hijack?: boolean;
  onHijackExit?: "complete" | "reopen";
  agent?: AgentLike | AgentLike[];
  prompt?: string;
  staticPayload?: unknown;
  computeFn?: () => unknown | Promise<unknown>;
  label?: string;
  meta?: Record<string, unknown>;
  scorers?: ScorersMap;
  memoryConfig?: TaskMemoryConfig;
};

type RetryPolicy = {
  backoff?: "fixed" | "linear" | "exponential";   // default "fixed"
  initialDelayMs?: number;                         // default 0
};

type CachePolicy<Ctx = unknown> = {
  by?: (ctx: Ctx) => unknown;
  version?: string;
  key?: string;
  ttlMs?: number;
  scope?: "run" | "workflow" | "global";
  [key: string]: unknown;
};

type AgentToolDescriptor = {
  description?: string;
  source?: "builtin" | "mcp" | "extension" | "skill" | "runtime";
};

type AgentCapabilityRegistry = {
  version: 1;
  engine: "claude-code" | "codex" | "antigravity" | "gemini" | "kimi" | "pi" | "amp" | "forge" | "opencode" | "openclaw" | "vibe";
  runtimeTools: Record<string, AgentToolDescriptor>;
  mcp: {
    bootstrap: "inline-config" | "project-config" | "allow-list" | "unsupported";
    supportsProjectScope: boolean;
    supportsUserScope: boolean;
  };
  skills: {
    supportsSkills: boolean;
    installMode?: "files" | "dir" | "plugin";
    smithersSkillIds: string[];
  };
  humanInteraction: {
    supportsUiRequests: boolean;
    methods: string[];
  };
  builtIns: string[];
};

type AgentGenerateOptions = {
  prompt?: unknown;
  messages?: unknown;
  timeout?: unknown;
  abortSignal?: AbortSignal;
  rootDir?: string;
  resumeSession?: string;
  maxOutputBytes?: number;
  onStdout?: (text: string) => void;
  onStderr?: (text: string) => void;
  onEvent?: (event: unknown) => unknown;
  retry?: unknown;
  isRetry?: unknown;
  retryAttempt?: unknown;
  schemaRetry?: unknown;
  taskContext?: {
    runId?: string;
    nodeId?: string;
    iteration?: number;
    attempt?: number;
  };
  [key: string]: unknown;
};

type AgentLike = {
  id?: string;
  tools?: Record<string, unknown>;
  supportsNativeStructuredOutput?: boolean;
  capabilities?: AgentCapabilityRegistry;
  generate: (args?: AgentGenerateOptions) => Promise<unknown>;
};

type SdkAgentOptions<CALL_OPTIONS = never, TOOLS extends import("ai").ToolSet = {}, MODEL = any> =
  Omit<import("ai").ToolLoopAgentSettings<CALL_OPTIONS, TOOLS, any, never>, "model"> & {
    model: string | MODEL;
  };

type AnthropicAgentOptions<CALL_OPTIONS = never, TOOLS extends import("ai").ToolSet = {}> =
  SdkAgentOptions<CALL_OPTIONS, TOOLS, import("ai").LanguageModel>;

type OpenAIAgentOptions<CALL_OPTIONS = never, TOOLS extends import("ai").ToolSet = {}> =
  Omit<SdkAgentOptions<CALL_OPTIONS, TOOLS, import("ai").LanguageModel>, "model"> & {
    nativeStructuredOutput?: boolean;
  } & (
    | { model: string; baseURL?: string; apiKey?: string }
    | { model: import("ai").LanguageModel; baseURL?: never; apiKey?: never }
  );

type HermesAgentOptions<CALL_OPTIONS = never, TOOLS extends import("ai").ToolSet = {}> =
  Omit<SdkAgentOptions<CALL_OPTIONS, TOOLS, import("ai").LanguageModel>, "model"> & {
    model?: string;                 // default "hermes"
    baseURL?: string;               // falls back to HERMES_BASE_URL; required at runtime
    apiKey?: string;                // falls back to HERMES_API_KEY, then "hermes"
    nativeStructuredOutput?: boolean; // default false
  };

type BaseCliAgentOptions = {
  id?: string;
  model?: string;
  systemPrompt?: string;
  instructions?: string;
  cwd?: string;
  env?: Record<string, string>;
  yolo?: boolean;
  timeoutMs?: number;
  idleTimeoutMs?: number;
  maxOutputBytes?: number;
  extraArgs?: string[];
};

type PiExtensionUiRequest = {
  type: "extension_ui_request";
  id: string;
  method: string;
  title?: string;
  placeholder?: string;
  [key: string]: unknown;
};

type PiExtensionUiResponse = {
  type: "extension_ui_response";
  id: string;
  value?: string;
  cancelled?: boolean;
  [key: string]: unknown;
};

type PiAgentOptions = BaseCliAgentOptions & {
  provider?: string;
  model?: string;
  apiKey?: string;
  systemPrompt?: string;
  appendSystemPrompt?: string;
  mode?: "text" | "json" | "rpc";
  print?: boolean;
  continue?: boolean;
  resume?: boolean;
  session?: string;
  sessionDir?: string;
  noSession?: boolean;
  models?: string | string[];
  listModels?: boolean | string;
  tools?: string[];
  noTools?: boolean;
  extension?: string[];
  noExtensions?: boolean;
  skill?: string[];
  noSkills?: boolean;
  promptTemplate?: string[];
  noPromptTemplates?: boolean;
  theme?: string[];
  noThemes?: boolean;
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
  export?: string;
  files?: string[];
  verbose?: boolean;
  onExtensionUiRequest?: (
    request: PiExtensionUiRequest,
  ) => Promise<PiExtensionUiResponse | null> | PiExtensionUiResponse | null;
};

type VibeAgentOptions = BaseCliAgentOptions & {
  agent?: string;
  maxTurns?: number;
  maxPrice?: number;
  maxTokens?: number;
  enabledTools?: string[];
  sessionId?: string;
  continueSession?: boolean;
};

type OpenCodeAgentOptions = BaseCliAgentOptions & {
  model?: string;
  agentName?: string;
  attachFiles?: string[];
  continueSession?: boolean;
  sessionId?: string;
  variant?: string;
};

type TaskMemoryConfig = {
  namespace?: string | MemoryNamespace;
  recall?: { namespace?: MemoryNamespace; query?: string; topK?: number };
  remember?: { namespace?: MemoryNamespace; key?: string };
  threadId?: string;
};

type MemoryNamespace = { kind: MemoryNamespaceKind; id: string };
type MemoryNamespaceKind = "workflow" | "agent" | "user" | "global";

// =============================================================================
// Graph
// =============================================================================

type GraphSnapshot = {
  readonly runId: string;
  readonly frameNo: number;
  readonly xml: XmlNode | null;
  readonly tasks: readonly TaskDescriptor[];
};

type XmlNode = XmlElement | XmlText;

type XmlElement = {
  readonly kind: "element";
  readonly tag: string;             // "Workflow" | "Task" | "Sequence" | ...
  readonly props: Record<string, string>;
  readonly children: readonly XmlNode[];
};

type XmlText = { readonly kind: "text"; readonly text: string };

// =============================================================================
// Events
// =============================================================================
//
// `SmithersEvent` is the discriminated union understood by the runtime and
// observability layer. Most variants are emitted by the runtime; reserved
// variants are called out in Event Types. The full union is documented
// separately to keep this file usable as the everyday type reference.
//
// See: /reference/event-types (rendered) or /llms-full.txt (LLM bundle).

type SmithersEvent = { type: string; runId: string; timestampMs: number } & Record<string, unknown>;
// (Each variant has additional fields per its `type`. See event-types.)

// =============================================================================
// Component props
// =============================================================================

type WorkflowProps = {
  name: string;
  cache?: boolean;
  children?: React.ReactNode;
};

// OutputTarget accepts a Zod output schema (recommended, usually outputs.key),
// a custom Drizzle table object, or a string schema key escape hatch.
type OutputTarget = import("zod").ZodObject<any> | { $inferSelect: any } | string;
type DepsSpec = Record<string, OutputTarget>;
type InferDeps<D extends DepsSpec> = {
  [K in keyof D]: D[K] extends string ? unknown : InferOutputEntry<D[K]>;
};

type TaskProps<Row, Output extends OutputTarget = OutputTarget, D extends DepsSpec = {}> = {
  key?: string;
  id: string;
  output: Output;
  outputSchema?: import("zod").ZodObject<any>;
  agent?: AgentLike | AgentLike[];
  fallbackAgent?: AgentLike;
  dependsOn?: string[];
  needs?: Record<string, string>;
  deps?: D;
  fork?: string;                    // start from another task's final agent session snapshot
  skipIf?: boolean;
  needsApproval?: boolean;
  async?: boolean;                  // only with needsApproval
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;       // alias for heartbeatTimeoutMs
  noRetry?: boolean;
  retries?: number;                 // default Infinity (set 0 to disable)
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
  cache?: CachePolicy;
  scorers?: ScorersMap;
  memory?: TaskMemoryConfig;
  hijack?: boolean;
  onHijackExit?: "complete" | "reopen";
  allowTools?: string[];            // CLI-agent tool allowlist
  label?: string;
  meta?: Record<string, unknown>;
  // string = prompt literal; Row = static result; () => Row = compute fn;
  // (deps) => result = deps-aware fn; React.ReactNode = JSX subtree
  children: string | Row | (() => Row | Promise<Row>) | React.ReactNode | ((deps: InferDeps<D>) => Row | React.ReactNode);
};

type SequenceProps  = { key?: string; skipIf?: boolean; children?: React.ReactNode };
type ParallelProps  = { id?: string; maxConcurrency?: number; subtreeConcurrency?: number; skipIf?: boolean; children?: React.ReactNode };
type BranchProps    = { if: boolean; then: React.ReactElement; else?: React.ReactElement | null; skipIf?: boolean };
type LoopProps      = {
  key?: string;
  id?: string;
  until?: boolean;
  maxIterations?: number;
  onMaxReached?: "fail" | "return-last";   // default "return-last"
  continueAsNewEvery?: number;
  skipIf?: boolean;
  children?: React.ReactNode;
};
type RalphProps     = LoopProps;            // deprecated alias

type ApprovalDecision  = { approved: boolean; note: string | null; decidedBy: string | null; decidedAt: string | null };
type ApprovalSelection = { selected: string; notes: string | null };
type ApprovalRanking   = { ranked: string[]; notes: string | null };
type ApprovalRequest   = { title: string; summary?: string; metadata?: Record<string, unknown> };
type ApprovalMode      = "approve" | "select" | "rank";
type ApprovalOption    = { key: string; label: string; summary?: string; metadata?: Record<string, unknown> };
type ApprovalAutoApprove = {
  after?: number;
  condition?: ((ctx: SmithersCtx<unknown> | null) => boolean) | (() => boolean);
  audit?: boolean;
  revertOn?: ((ctx: SmithersCtx<unknown> | null) => boolean) | (() => boolean);
};

type ApprovalProps<Row = ApprovalDecision, Output extends OutputTarget = OutputTarget> = {
  id: string;
  mode?: ApprovalMode;
  options?: ApprovalOption[];
  output: Output;
  outputSchema?: import("zod").ZodObject<any>;
  request: ApprovalRequest;
  onDeny?: "fail" | "continue" | "skip";
  allowedScopes?: string[];
  allowedUsers?: string[];
  autoApprove?: ApprovalAutoApprove;
  async?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;       // alias for heartbeatTimeoutMs
  retries?: number;
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
  cache?: CachePolicy;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: React.ReactNode;
};

type SignalProps<S extends import("zod").ZodObject<any> = import("zod").ZodObject<any>> = {
  id: string;
  schema: S;
  correlationId?: string;
  timeoutMs?: number;
  onTimeout?: "fail" | "skip" | "continue";
  async?: boolean;
  skipIf?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: (data: import("zod").infer<S>) => React.ReactNode;
};

type WaitForEventProps = {
  id: string;
  event: string;
  correlationId?: string;
  output: OutputTarget;
  outputSchema?: import("zod").ZodObject<any>;
  timeoutMs?: number;
  onTimeout?: "fail" | "skip" | "continue";
  async?: boolean;
  skipIf?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
};

type TimerProps = {
  id: string;
  duration?: string;                // e.g. "30s", "5m"
  until?: string | Date;            // absolute timestamp
  every?: string;                   // reserved; recurring timers are not supported yet
  skipIf?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
};

type SagaStepDef    = { id: string; action: React.ReactElement; compensation: React.ReactElement; label?: string };
type SagaProps      = { id?: string; steps?: SagaStepDef[]; onFailure?: "compensate" | "compensate-and-fail" | "fail"; skipIf?: boolean; children?: React.ReactNode };
type SagaStepProps  = { id: string; compensation: React.ReactElement; children: React.ReactElement };

type TryCatchFinallyProps = {
  id?: string;
  try: React.ReactElement;
  catch?: React.ReactElement | ((error: SmithersError) => React.ReactElement);
  catchErrors?: SmithersErrorCode[];
  finally?: React.ReactElement;
  skipIf?: boolean;
};

// Higher-level composites
type PollerProps = {
  id?: string;
  check: AgentLike | (() => unknown | Promise<unknown>);
  checkOutput: OutputTarget;
  maxAttempts?: number;
  backoff?: "fixed" | "linear" | "exponential";
  intervalMs?: number;
  onTimeout?: "fail" | "return-last";
  skipIf?: boolean;
  children?: React.ReactNode;
};

type ColumnTaskProps = Omit<Partial<TaskProps<unknown>>, "agent" | "children" | "id" | "key" | "output" | "smithersContext">;

type ColumnDef = {
  name: string;
  agent: AgentLike;
  output: OutputTarget;
  prompt?: (ctx: { item: unknown; column: string }) => string;
  task?: ColumnTaskProps;
};

type KanbanProps = {
  id?: string;
  columns: ColumnDef[];
  useTickets: () => Array<{ id: string; [key: string]: unknown }>;
  agents?: Record<string, AgentLike>;
  maxConcurrency?: number;
  onComplete?: OutputTarget;
  until?: boolean;
  maxIterations?: number;
  skipIf?: boolean;
  children?: React.ReactNode | Record<string, unknown>;
};

type ApprovalGateProps = {
  id: string;
  output: OutputTarget;
  request: ApprovalRequest;
  when: boolean;                    // false auto-approves
  onDeny?: "fail" | "continue" | "skip";
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;
  retries?: number;
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
};

type HumanTaskProps = {
  id: string;
  output: OutputTarget;
  outputSchema?: import("zod").ZodObject<any>;
  prompt: string | React.ReactNode;
  maxAttempts?: number;
  async?: boolean;
  skipIf?: boolean;
  timeoutMs?: number;
  continueOnFail?: boolean;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
};

type CheckConfig = {
  id: string;
  agent?: AgentLike;
  command?: string;
  label?: string;
};
type CheckSuiteProps = {
  id?: string;
  checks: CheckConfig[] | Record<string, Omit<CheckConfig, "id">>;
  verdictOutput: OutputTarget;
  strategy?: "all-pass" | "majority" | "any-pass";
  maxConcurrency?: number;
  continueOnFail?: boolean;
  skipIf?: boolean;
};

type TokenBudgetConfig = { max: number; perTask?: number; onExceeded?: "fail" | "warn" | "skip-remaining" };
type LatencySloConfig = { maxMs: number; perTask?: number; onExceeded?: "fail" | "warn" };
type TrackingConfig = { tokens?: boolean; latency?: boolean };
type AspectsProps = {
  tokenBudget?: TokenBudgetConfig;
  latencySlo?: LatencySloConfig;
  tracking?: TrackingConfig;
  children?: React.ReactNode;
};

type CategoryConfig = {
  agent: AgentLike;
  output?: OutputTarget;
  prompt?: (item: unknown) => string;
};
type ClassifyAndRouteProps = {
  id?: string;
  items: unknown | unknown[];
  categories: Record<string, AgentLike | CategoryConfig>;
  classifierAgent: AgentLike;
  classifierOutput: OutputTarget;
  routeOutput: OutputTarget;
  classificationResult?: {
    classifications: Array<{ itemId?: string; category: string; [key: string]: unknown }>;
  } | null;
  maxConcurrency?: number;
  skipIf?: boolean;
  children?: React.ReactNode;
};

type SourceDef = {
  agent: AgentLike;
  prompt?: string;
  output?: OutputTarget;
  children?: React.ReactNode;
};
type GatherAndSynthesizeProps = {
  id?: string;
  sources: Record<string, SourceDef>;
  synthesizer: AgentLike;
  gatherOutput: OutputTarget;
  synthesisOutput: OutputTarget;
  gatheredResults?: Record<string, unknown> | null;
  maxConcurrency?: number;
  synthesisPrompt?: string;
  skipIf?: boolean;
  children?: React.ReactNode;
};

type ContentPipelineStage = { id: string; agent: AgentLike; output: OutputTarget; label?: string };
type ContentPipelineProps = {
  id?: string;
  stages: ContentPipelineStage[];
  skipIf?: boolean;
  children: string | React.ReactNode;
};

type DebateProps = {
  id?: string;
  proposer: AgentLike;
  opponent: AgentLike;
  judge: AgentLike;
  rounds?: number;
  argumentOutput: OutputTarget;
  verdictOutput: OutputTarget;
  topic: string | React.ReactNode;
  skipIf?: boolean;
};

type DecisionRule = { when: boolean; then: React.ReactElement; label?: string };
type DecisionTableProps = {
  id?: string;
  rules: DecisionRule[];
  default?: React.ReactElement;
  strategy?: "first-match" | "all-match";
  skipIf?: boolean;
};

type DriftDetectorProps = {
  id?: string;
  captureAgent: AgentLike;
  compareAgent: AgentLike;
  captureOutput: OutputTarget;
  compareOutput: OutputTarget;
  baseline: unknown;
  alertIf?: (comparison: unknown) => boolean;
  alert?: React.ReactElement;
  poll?: { intervalMs?: number; maxPolls?: number };
  skipIf?: boolean;
};

type EscalationLevel = {
  agent: AgentLike;
  output: OutputTarget;
  label?: string;
  escalateIf?: (result: unknown) => boolean;
};
type EscalationChainProps = {
  id?: string;
  levels: EscalationLevel[];
  humanFallback?: boolean;
  humanRequest?: ApprovalRequest;
  escalationOutput: OutputTarget;
  skipIf?: boolean;
  children?: React.ReactNode;
};

type MergeQueueProps = { id?: string; maxConcurrency?: number; skipIf?: boolean; children?: React.ReactNode };

type OptimizerProps = {
  id?: string;
  generator: AgentLike;
  evaluator: AgentLike | ((candidate: unknown) => unknown | Promise<unknown>);
  generateOutput: OutputTarget;
  evaluateOutput: OutputTarget;
  targetScore?: number;
  maxIterations?: number;
  onMaxReached?: "return-last" | "fail";
  skipIf?: boolean;
  children: string | React.ReactNode;
};

type PanelistConfig = { agent: AgentLike | AgentLike[]; role?: string; label?: string };

type PanelProps = {
  id?: string;
  // Each entry is an agent, a PanelistConfig, or a failover chain (AgentLike[]);
  // a chain becomes one panelist run as failover.
  panelists: Array<PanelistConfig | AgentLike | AgentLike[]>;
  // the synthesizing moderator; a chain (AgentLike[]) runs as failover.
  moderator: AgentLike | AgentLike[];
  panelistOutput: OutputTarget;
  moderatorOutput: OutputTarget;
  strategy?: "synthesize" | "vote" | "consensus";
  minAgree?: number;
  maxConcurrency?: number;
  // extra Task props for each panelist / the moderator (continueOnFail, timeouts)
  panelistTaskProps?: { continueOnFail?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number };
  moderatorTaskProps?: { continueOnFail?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number };
  skipIf?: boolean;
  children: string | React.ReactNode;
};

type SidecarProps = {
  id?: string;
  agent: AgentLike;
  sidecar: AgentLike;
  output: OutputTarget;
  sidecarOutput?: OutputTarget;
  scorers?: ScorersMap;
  prompt?: string | React.ReactNode;
  input?: string | React.ReactNode;
  maxConcurrency?: number;
  groundTruth?: unknown;
  context?: unknown;
  primaryLabel?: string;
  sidecarLabel?: string;
  skipIf?: boolean;
  children?: string | React.ReactNode;
};
type SidecarDelta = {
  primaryScore: number | null;
  sidecarScore: number | null;
  delta: number | null;
  cheaperWins: boolean;
};

type ReviewLoopProps = {
  id?: string;
  producer: AgentLike;
  reviewer: AgentLike | AgentLike[];
  produceOutput: OutputTarget;
  reviewOutput: OutputTarget;
  maxIterations?: number;
  onMaxReached?: "return-last" | "fail";
  skipIf?: boolean;
  children: string | React.ReactNode;
};

type RunbookStep = {
  id: string;
  agent?: AgentLike;
  command?: string;
  risk: "safe" | "risky" | "critical";
  label?: string;
  output?: OutputTarget;
};
type RunbookProps = {
  id?: string;
  steps: RunbookStep[];
  defaultAgent?: AgentLike;
  stepOutput: OutputTarget;
  approvalRequest?: Partial<ApprovalRequest>;
  onDeny?: "fail" | "skip";
  skipIf?: boolean;
};

type ScanFixVerifyProps = {
  id?: string;
  scanner: AgentLike;
  fixer: AgentLike | AgentLike[];
  verifier: AgentLike;
  scanOutput: OutputTarget;
  fixOutput: OutputTarget;
  verifyOutput: OutputTarget;
  reportOutput: OutputTarget;
  maxConcurrency?: number;
  maxRetries?: number;
  skipIf?: boolean;
  children?: React.ReactNode;
};

type SupervisorProps = {
  id?: string;
  boss: AgentLike;
  workers: Record<string, AgentLike>;
  planOutput: OutputTarget;
  workerOutput: OutputTarget;
  reviewOutput: OutputTarget;
  finalOutput: OutputTarget;
  maxIterations?: number;
  maxConcurrency?: number;
  useWorktrees?: boolean;
  skipIf?: boolean;
  children: string | React.ReactNode;
};

type ContinueAsNewProps = { state?: unknown };

// Sandbox
type SandboxRuntime = "bubblewrap" | "docker" | "codeplane" | "cloudflare";
type SandboxEgressConfig = {
  env?: Record<string, string>;
  httpProxy?: string;
  httpsProxy?: string;
  noProxy?: string | string[];
  caCertPem?: string;
  caCertPath?: string;
  secretBindings?: Record<string, string>;
};
type SandboxVolumeMount = { host: string; container: string; readonly?: boolean };
type SandboxWorkspaceSpec = {
  name: string;
  snapshotId?: string;
  idleTimeoutSecs?: number;
  persistence?: "ephemeral" | "sticky";
};
type SandboxWorkflow = {
  db?: unknown;
  build: (ctx: unknown) => unknown;
  opts?: Record<string, unknown>;
  schemaRegistry?: unknown;
  zodToKeyName?: unknown;
};
type SandboxChildWorkflowDefinition =
  | SandboxWorkflow
  | (() => SandboxWorkflow | unknown);
type ExecuteSandboxChildWorkflowOptions = {
  workflow: SandboxChildWorkflowDefinition;
  input?: unknown;
  runId?: string;
  parentRunId?: string;
  rootDir?: string;
  allowNetwork?: boolean;
  maxOutputBytes?: number;
  toolTimeoutMs?: number;
  workflowPath?: string;
  signal?: AbortSignal;
};
type ExecuteSandboxChildWorkflow = (
  parentWorkflow: SandboxWorkflow | undefined,
  options: ExecuteSandboxChildWorkflowOptions,
) => Promise<{ runId: string; status: string; output: unknown }>;
type SandboxDiffBundleLike = {
  seq: number;
  baseRef: string;
  patches: Array<{
    path: string;
    operation: "add" | "modify" | "delete";
    diff: string;
    binaryContent?: string;
  }>;
};
type SandboxProviderRequest = {
  runId: string;
  sandboxId: string;
  input?: unknown;
  rootDir: string;
  requestBundlePath: string;
  resultBundlePath: string;
  workflow: SandboxChildWorkflowDefinition;
  parentWorkflow?: SandboxWorkflow;
  executeChildWorkflow: ExecuteSandboxChildWorkflow;
  allowNetwork: boolean;
  maxOutputBytes: number;
  toolTimeoutMs: number;
  egress?: SandboxEgressConfig;
  config: Record<string, unknown>;
  signal?: AbortSignal;
  heartbeat: (data?: unknown) => void;
};
type SandboxProviderResult =
  | { bundlePath: string; remoteRunId?: string; workspaceId?: string; containerId?: string }
  | {
      status: "finished" | "failed" | "cancelled";
      output?: unknown;
      outputs?: unknown;
      runId?: string;
      remoteRunId?: string;
      workspaceId?: string;
      containerId?: string;
      diffBundle?: SandboxDiffBundleLike;
      patches?: Array<{ path: string; content: string }>;
      artifacts?: Array<{ path: string; content: string }>;
      streamLogPath?: string | null;
    };
type SandboxProvider = {
  id: string;
  run(request: SandboxProviderRequest): Promise<SandboxProviderResult> | SandboxProviderResult;
  cleanup?(request: SandboxProviderRequest): Promise<void> | void;
};
type ExecuteSandboxOptions = {
  parentWorkflow?: SandboxWorkflow;
  sandboxId: string;
  provider?: SandboxProvider | string;
  runtime?: SandboxRuntime;
  workflow: SandboxChildWorkflowDefinition;
  executeChildWorkflow: ExecuteSandboxChildWorkflow;
  applyDiffBundle?: (bundle: SandboxDiffBundleLike, targetDir: string) => Promise<void>;
  input?: unknown;
  rootDir: string;
  allowNetwork: boolean;
  maxOutputBytes: number;
  toolTimeoutMs: number;
  reviewDiffs?: boolean;
  autoAcceptDiffs?: boolean;
  allowNested?: boolean;
  config?: Record<string, unknown>;
};

type SandboxProps = {
  id: string;
  workflow?: SmithersWorkflow<unknown>;
  input?: unknown;
  output: OutputTarget;
  provider?: unknown;              // runtime accepts a provider object or registered provider id
  runtime?: SandboxRuntime;            // legacy local transports
  allowNetwork?: boolean;
  reviewDiffs?: boolean;
  autoAcceptDiffs?: boolean;
  allowNested?: boolean;
  image?: string;
  env?: Record<string, string>;
  egress?: SandboxEgressConfig;
  ports?: Array<{ host: number; container: number }>;
  volumes?: SandboxVolumeMount[];
  memoryLimit?: string;
  cpuLimit?: string;
  command?: string;
  workspace?: SandboxWorkspaceSpec;
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;       // alias for heartbeatTimeoutMs
  retries?: number;
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
  cache?: CachePolicy;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: React.ReactNode;
};

type SubflowProps = {
  id: string;
  workflow: SmithersWorkflow<unknown>;
  input?: unknown;
  mode?: "childRun" | "inline";
  output: OutputTarget;
  skipIf?: boolean;
  timeoutMs?: number;
  heartbeatTimeoutMs?: number;
  heartbeatTimeout?: number;       // alias for heartbeatTimeoutMs
  retries?: number;
  retryPolicy?: RetryPolicy;
  continueOnFail?: boolean;
  cache?: CachePolicy;
  dependsOn?: string[];
  needs?: Record<string, string>;
  label?: string;
  meta?: Record<string, unknown>;
  key?: string;
  children?: React.ReactNode;
};

type WorktreeProps = {
  key?: string;
  id?: string;
  path: string;
  branch?: string;
  baseBranch?: string;             // default "main"
  skipIf?: boolean;
  children?: React.ReactNode;
};

type SuperSmithersProps = {
  id?: string;                     // default "super-smithers"; prefixes internal task ids
  strategy: string | React.ReactElement;
  agent: AgentLike;
  targetFiles?: string[];
  reportOutput?: OutputTarget;
  dryRun?: boolean;                // default false
  skipIf?: boolean;
};

// =============================================================================
// Errors
// =============================================================================
//
// Every Smithers error is a SmithersError with a typed code. See the Errors page
// for the full list of built-in codes.

declare class SmithersError extends Error {
  readonly code: SmithersErrorCode;
  readonly summary: string;
  readonly docsUrl: string;
  readonly details?: Record<string, unknown>;
  readonly cause?: unknown;
}

type SmithersErrorCode = KnownSmithersErrorCode | (string & {});
type KnownSmithersErrorCode =
  | "INVALID_INPUT" | "MISSING_INPUT" | "MISSING_INPUT_TABLE" | "RESUME_METADATA_MISMATCH"
  | "UNKNOWN_OUTPUT_SCHEMA" | "INVALID_OUTPUT" | "WORKTREE_CREATE_FAILED" | "VCS_NOT_FOUND"
  | "SNAPSHOT_NOT_FOUND" | "VCS_WORKSPACE_CREATE_FAILED" | "TASK_TIMEOUT"
  | "TASK_HIJACK_UNSUPPORTED" | "TASK_FORK_SOURCE_NOT_FOUND" | "TASK_FORK_SOURCE_NOT_COMPLETE"
  | "TASK_FORK_SESSION_UNAVAILABLE" | "TASK_FORK_CYCLE" | "RUN_NOT_FOUND" | "NODE_NOT_FOUND" | "INVALID_EVENTS_OPTIONS"
  | "SANDBOX_BUNDLE_INVALID" | "SANDBOX_BUNDLE_TOO_LARGE" | "WORKFLOW_EXECUTION_FAILED"
  | "SANDBOX_EXECUTION_FAILED" | "TASK_HEARTBEAT_TIMEOUT" | "HEARTBEAT_PAYLOAD_TOO_LARGE"
  | "HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE" | "TASK_ABORTED" | "RUN_CANCELLED" | "RUN_NOT_RESUMABLE"
  | "RUN_OWNER_ALIVE" | "RUN_STILL_RUNNING" | "RUN_RESUME_CLAIM_LOST" | "RUN_RESUME_CLAIM_FAILED"
  | "RUN_RESUME_ACTIVATION_FAILED" | "RUN_HIJACKED" | "CONTINUATION_STATE_TOO_LARGE"
  | "INVALID_CONTINUATION_STATE" | "RALPH_MAX_REACHED" | "SCHEDULER_ERROR" | "SESSION_ERROR"
  | "TASK_ID_REQUIRED" | "TASK_MISSING_OUTPUT" | "TASK_EMPTY_PROMPT" | "WORKFLOW_RENDER_FAILED" | "DUPLICATE_ID" | "NESTED_LOOP"
  | "WORKTREE_EMPTY_PATH" | "MDX_PRELOAD_INACTIVE" | "CONTEXT_OUTSIDE_WORKFLOW"
  | "MISSING_OUTPUT" | "DEP_NOT_SATISFIED" | "ASPECT_BUDGET_EXCEEDED" | "APPROVAL_OUTSIDE_TASK"
  | "APPROVAL_OPTIONS_REQUIRED" | "WORKFLOW_MISSING_DEFAULT" | "WORKFLOW_NOT_BUILT"
  | "TOOL_PATH_INVALID" | "TOOL_PATH_ESCAPE" | "TOOL_FILE_TOO_LARGE" | "TOOL_CONTENT_TOO_LARGE"
  | "TOOL_PATCH_TOO_LARGE" | "TOOL_PATCH_FAILED" | "TOOL_NETWORK_DISABLED"
  | "TOOL_GIT_REMOTE_DISABLED" | "TOOL_COMMAND_FAILED" | "TOOL_GREP_FAILED"
  | "AGENT_CLI_ERROR" | "AGENT_QUOTA_EXCEEDED" | "AGENT_CONFIG_INVALID" | "AGENT_RPC_FILE_ARGS" | "AGENT_BUILD_COMMAND" | "AGENT_DIAGNOSTIC_TIMEOUT"
  | "DB_MISSING_COLUMNS" | "DB_REQUIRES_BUN_SQLITE" | "DB_QUERY_FAILED" | "DB_WRITE_FAILED"
  | "SMITHERS_BACKEND_CONFLICT" | "SMITHERS_MIGRATION_REQUIRED"
  | "STORAGE_ERROR" | "INTERNAL_ERROR" | "PROCESS_ABORTED" | "PROCESS_TIMEOUT"
  | "PROCESS_IDLE_TIMEOUT" | "PROCESS_SPAWN_FAILED" | "TASK_RUNTIME_UNAVAILABLE"
  | "SCHEMA_CHANGE_HOT" | "HOT_OVERLAY_FAILED" | "HOT_RELOAD_INVALID_MODULE"
  | "SCORER_FAILED" | "WORKFLOW_EXISTS" | "CLI_DB_NOT_FOUND" | "CLI_AGENT_UNSUPPORTED"
  | "PI_HTTP_ERROR" | "EXTERNAL_BUILD_FAILED" | "SCHEMA_DISCOVERY_FAILED"
  | "OPENAPI_SPEC_LOAD_FAILED" | "OPENAPI_OPERATION_NOT_FOUND" | "OPENAPI_TOOL_EXECUTION_FAILED"
  | "ACCOUNT_INVALID" | "ACCOUNT_NOT_FOUND" | "ACCOUNT_DUPLICATE_LABEL" | "ACCOUNTS_FILE_INVALID";

// =============================================================================
// Server
// =============================================================================

type SmithersDb = import("@smithers-orchestrator/db/adapter").SmithersDb;

type ServerOptions = {
  port?: number;
  db?: unknown;
  authToken?: string;
  maxBodyBytes?: number;
  rootDir?: string;
  allowNetwork?: boolean;
  headersTimeout?: number;
  requestTimeout?: number;
};

type ServeOptions = {
  workflow: SmithersWorkflow<unknown>;
  adapter: SmithersDb;
  runId: string;
  abort: AbortController;
  authToken?: string;
  metrics?: boolean;
};

type GatewayTokenGrant = {
  role: string;
  scopes: string[];
  userId?: string;
  tokenId?: string;
  issuedAtMs?: number;
  expiresAtMs?: number;
  revokedAtMs?: number;
};
type GatewayAuthConfig =
  | {
      mode: "token";
      tokens: Record<string, GatewayTokenGrant>;
      allowedOrigins?: string[];     // default [] (no Origin allowlist)
    }
  | {
      mode: "jwt";
      issuer: string;
      audience: string | string[];
      secret: string;
      scopesClaim?: string;          // default "scope"
      roleClaim?: string;            // default "role"
      userClaim?: string;            // default "sub"
      defaultRole?: string;          // default "operator"
      defaultScopes?: string[];      // default [] when scope claim is absent
      clockSkewSeconds?: number;     // default 60; negative values clamp to 0
      allowedOrigins?: string[];     // default [] (no Origin allowlist)
    }
  | {
      mode: "trusted-proxy";
      trustedHeaders?: string[];     // default ["x-user-id","x-user-scopes","x-user-role"]
      allowedOrigins?: string[];     // default [] (no Origin allowlist)
      defaultRole?: string;          // default "operator"
      defaultScopes?: string[];      // trusted-proxy: used when the scopes header is absent, else the request is rejected
    };
type GatewayDefaults = { cliAgentTools?: "all" | "explicit-only" };

type GatewayOperatorUiConfig = {
  path?: string;                    // default "/console"
  title?: string;
  props?: Record<string, unknown>;
};

type GatewayUiConfig =
  | true
  | {
      entry: string;
      path?: string;                // gateway default "/"; workflow default "/workflows/<workflowKey>"
      title?: string;
      props?: Record<string, unknown>;
    };

type GatewayWebhookSignalConfig = {
  name: string;
  correlationIdPath?: string;
  runIdPath?: string;
  payloadPath?: string;
};

type GatewayWebhookRunConfig = {
  enabled?: boolean;
  inputPath?: string;
};

type GatewayWebhookConfig = {
  secret: string;
  signatureHeader?: string;
  signaturePrefix?: string;
  signal?: GatewayWebhookSignalConfig;
  run?: GatewayWebhookRunConfig;
};

type GatewayRegisterOptions = {
  schedule?: string;
  webhook?: GatewayWebhookConfig;
  ui?: GatewayUiConfig;
};

type GatewayOptions = {
  protocol?: number;
  features?: string[];
  heartbeatMs?: number;
  auth?: GatewayAuthConfig;
  ui?: GatewayUiConfig;
  operatorUi?: GatewayOperatorUiConfig | false;
  defaults?: GatewayDefaults;
  maxBodyBytes?: number;
  maxPayload?: number;
  maxConnections?: number;
  eventWindowSize?: number;
  headersTimeout?: number;
  requestTimeout?: number;
};

// =============================================================================
// Scorers (smithers-orchestrator/scorers)
// =============================================================================

type ScoreResult  = { score: number; reason?: string; meta?: Record<string, unknown> };
type ScorerInput  = { input: unknown; output: unknown; groundTruth?: unknown; context?: unknown; latencyMs?: number; outputSchema?: import("zod").ZodObject<any> };
type ScorerFn     = (input: ScorerInput) => Promise<ScoreResult>;
type Scorer       = { id: string; name: string; description: string; score: ScorerFn };
type SamplingConfig =
  | { type: "all" }
  | { type: "ratio"; rate: number }
  | { type: "none" };
type ScorerBinding = { scorer: Scorer; sampling?: SamplingConfig };
type ScorersMap    = Record<string, ScorerBinding>;

type ScoreRow = {
  id: string;
  runId: string;
  nodeId: string;
  iteration: number;
  attempt: number;
  scorerId: string;
  scorerName: string;
  source: "live" | "batch";
  score: number;
  reason: string | null;
  metaJson: string | null;
  inputJson: string | null;
  outputJson: string | null;
  groundTruthJson: string | null;
  contextJson: string | null;
  latencyMs: number | null;
  scoredAtMs: number;
  durationMs: number | null;
};

type AggregateScore = {
  scorerId: string;
  scorerName: string;
  count: number;
  mean: number;
  min: number;
  max: number;
  p50: number;
  stddev: number;
};

type AggregateOptions = {
  runId?: string;
  nodeId?: string;
  scorerId?: string;
};

type ScorerContext = {
  runId: string;
  nodeId: string;
  iteration: number;
  attempt: number;
  input: unknown;
  output: unknown;
  latencyMs?: number;
  outputSchema?: import("zod").ZodObject<any>;
};

type LlmJudgeConfig = {
  id: string;
  name: string;
  description: string;
  judge: AgentLike;
  instructions: string;
  promptTemplate: (input: ScorerInput) => string;
};
type CreateScorerConfig = {
  id: string;
  name: string;
  description: string;
  score: ScorerFn;
};

// =============================================================================
// Memory (smithers-orchestrator/memory)
// =============================================================================

type MemoryFact    = { namespace: string; key: string; valueJson: string; schemaSig?: string | null; createdAtMs: number; updatedAtMs: number; ttlMs?: number | null };
type MemoryMessage = { id: string; threadId: string; role: string; contentJson: string; runId?: string | null; nodeId?: string | null; createdAtMs: number };
type MemoryThread  = { threadId: string; namespace: string; title?: string | null; metadataJson?: string | null; createdAtMs: number; updatedAtMs: number };

type MemoryProvenance = { runId?: string | null; nodeId?: string | null; iteration?: number | null };

type MemoryNote = {
  id: string;
  namespace: string;
  body: string;
  kind?: string | null;
  tagsJson?: string | null; // JSON-encoded string array; null when the note has no tags
  author?: string | null;
  status: string; // free-form; conventionally pending | accepted | rejected
  statusChangedAtMs?: number | null;
  createdAtMs: number;
  runId?: string | null;
  nodeId?: string | null;
  iteration?: number | null;
};

type SaveNoteInput = {
  namespace: MemoryNamespace;
  body: string;
  kind?: string;
  tags?: string[];
  author?: string;
  status?: string; // defaults to "accepted"
  provenance?: MemoryProvenance;
  supersedes?: string[]; // note ids this note replaces
  id?: string; // provide to make retries idempotent
};

type NoteReadFilter = {
  status?: string | string[] | "any";
  includeSuperseded?: boolean;
  kind?: string;
  namespace?: MemoryNamespace; // scope searchNotes to one namespace of the kind
};

type WorkingMemoryConfig<
  T extends import("zod").ZodObject<import("zod").ZodRawShape> = import("zod").ZodObject<import("zod").ZodRawShape>,
> = {
  schema?: T;
  namespace: MemoryNamespace;
  ttlMs?: number;
};

type SemanticRecallConfig = {
  topK?: number;
  namespace?: MemoryNamespace;
  similarityThreshold?: number;
};

type MessageHistoryConfig = {
  lastMessages?: number;
  threadId?: string;
};

type MemoryStore = {
  getFact(ns: MemoryNamespace, key: string): Promise<MemoryFact | undefined>;
  setFact(ns: MemoryNamespace, key: string, value: unknown, ttlMs?: number): Promise<void>;
  deleteFact(ns: MemoryNamespace, key: string): Promise<void>;
  listFacts(ns: MemoryNamespace): Promise<MemoryFact[]>;
  listAllFacts(): Promise<MemoryFact[]>;
  createThread(ns: MemoryNamespace, title?: string): Promise<MemoryThread>;
  getThread(threadId: string): Promise<MemoryThread | undefined>;
  deleteThread(threadId: string): Promise<void>;
  saveMessage(msg: Omit<MemoryMessage, "createdAtMs"> & { createdAtMs?: number }): Promise<void>;
  listMessages(threadId: string, limit?: number): Promise<MemoryMessage[]>;
  countMessages(threadId: string): Promise<number>;
  deleteExpiredFacts(): Promise<number>;
  saveNote(input: SaveNoteInput): Promise<MemoryNote>;
  getNote(id: string): Promise<MemoryNote | undefined>;
  listNotes(ns: MemoryNamespace, filter?: NoteReadFilter): Promise<MemoryNote[]>;
  setNoteStatus(id: string, status: string): Promise<void>;
  enableNoteSearch(kind: string): Promise<void>;
  searchNotes(kind: string, query: string, limit?: number, filter?: NoteReadFilter): Promise<MemoryNote[]>;
  getFactEffect(ns: MemoryNamespace, key: string): Effect.Effect<MemoryFact | undefined, SmithersError>;
  setFactEffect(ns: MemoryNamespace, key: string, value: unknown, ttlMs?: number): Effect.Effect<void, SmithersError>;
  deleteFactEffect(ns: MemoryNamespace, key: string): Effect.Effect<void, SmithersError>;
  listFactsEffect(ns: MemoryNamespace): Effect.Effect<MemoryFact[], SmithersError>;
  listAllFactsEffect(): Effect.Effect<MemoryFact[], SmithersError>;
  createThreadEffect(ns: MemoryNamespace, title?: string): Effect.Effect<MemoryThread, SmithersError>;
  getThreadEffect(threadId: string): Effect.Effect<MemoryThread | undefined, SmithersError>;
  deleteThreadEffect(threadId: string): Effect.Effect<void, SmithersError>;
  saveMessageEffect(msg: Omit<MemoryMessage, "createdAtMs"> & { createdAtMs?: number }): Effect.Effect<void, SmithersError>;
  listMessagesEffect(threadId: string, limit?: number): Effect.Effect<MemoryMessage[], SmithersError>;
  countMessagesEffect(threadId: string): Effect.Effect<number, SmithersError>;
  deleteExpiredFactsEffect(): Effect.Effect<number, SmithersError>;
  saveNoteEffect(input: SaveNoteInput): Effect.Effect<MemoryNote, SmithersError>;
  getNoteEffect(id: string): Effect.Effect<MemoryNote | undefined, SmithersError>;
  listNotesEffect(ns: MemoryNamespace, filter?: NoteReadFilter): Effect.Effect<MemoryNote[], SmithersError>;
  setNoteStatusEffect(id: string, status: string): Effect.Effect<void, SmithersError>;
  enableNoteSearchEffect(kind: string): Effect.Effect<void, SmithersError>;
  searchNotesEffect(kind: string, query: string, limit?: number, filter?: NoteReadFilter): Effect.Effect<MemoryNote[], SmithersError>;
};

type MemoryServiceApi = {
  readonly getFact: (ns: MemoryNamespace, key: string) => Effect.Effect<MemoryFact | undefined, SmithersError>;
  readonly setFact: (ns: MemoryNamespace, key: string, value: unknown, ttlMs?: number) => Effect.Effect<void, SmithersError>;
  readonly deleteFact: (ns: MemoryNamespace, key: string) => Effect.Effect<void, SmithersError>;
  readonly listFacts: (ns: MemoryNamespace) => Effect.Effect<MemoryFact[], SmithersError>;
  readonly createThread: (ns: MemoryNamespace, title?: string) => Effect.Effect<MemoryThread, SmithersError>;
  readonly getThread: (threadId: string) => Effect.Effect<MemoryThread | undefined, SmithersError>;
  readonly deleteThread: (threadId: string) => Effect.Effect<void, SmithersError>;
  readonly saveMessage: (msg: Omit<MemoryMessage, "createdAtMs"> & { createdAtMs?: number }) => Effect.Effect<void, SmithersError>;
  readonly listMessages: (threadId: string, limit?: number) => Effect.Effect<MemoryMessage[], SmithersError>;
  readonly countMessages: (threadId: string) => Effect.Effect<number, SmithersError>;
  readonly deleteExpiredFacts: () => Effect.Effect<number, SmithersError>;
  readonly saveNote: (input: SaveNoteInput) => Effect.Effect<MemoryNote, SmithersError>;
  readonly getNote: (id: string) => Effect.Effect<MemoryNote | undefined, SmithersError>;
  readonly listNotes: (ns: MemoryNamespace, filter?: NoteReadFilter) => Effect.Effect<MemoryNote[], SmithersError>;
  readonly setNoteStatus: (id: string, status: string) => Effect.Effect<void, SmithersError>;
  readonly enableNoteSearch: (kind: string) => Effect.Effect<void, SmithersError>;
  readonly searchNotes: (kind: string, query: string, limit?: number, filter?: NoteReadFilter) => Effect.Effect<MemoryNote[], SmithersError>;
  readonly store: MemoryStore;
};

type MemoryProcessorConfig = {
  processors?: string[];
};

type MemoryProcessor = {
  name: string;
  process: (store: MemoryStore) => Promise<void>;
  processEffect: (store: MemoryStore) => Effect.Effect<void, SmithersError>;
};

type MemoryLayerConfig = {
  db: import("drizzle-orm/bun-sqlite").BunSQLiteDatabase<Record<string, unknown>>;
};

// =============================================================================
// OpenAPI tools (smithers-orchestrator/openapi)
// =============================================================================

type OpenApiAuth =
  | { type: "apiKey"; name: string; in: "header" | "query"; value: string }
  | { type: "bearer"; token: string }
  | { type: "basic"; username: string; password: string };

type OpenApiRefObject = {
  $ref: string;
};

type OpenApiSchemaObject = {
  type?: string;
  format?: string;
  description?: string;
  properties?: Record<string, OpenApiSchemaObject | OpenApiRefObject>;
  required?: string[];
  items?: OpenApiSchemaObject | OpenApiRefObject;
  enum?: unknown[];
  default?: unknown;
  nullable?: boolean;
  oneOf?: Array<OpenApiSchemaObject | OpenApiRefObject>;
  anyOf?: Array<OpenApiSchemaObject | OpenApiRefObject>;
  allOf?: Array<OpenApiSchemaObject | OpenApiRefObject>;
  additionalProperties?: boolean | OpenApiSchemaObject | OpenApiRefObject;
  minimum?: number;
  maximum?: number;
  minLength?: number;
  maxLength?: number;
  pattern?: string;
  $ref?: string;
};

type OpenApiParameterObject = {
  name: string;
  in: "query" | "header" | "path" | "cookie";
  description?: string;
  required?: boolean;
  schema?: OpenApiSchemaObject | OpenApiRefObject;
  deprecated?: boolean;
};

type OpenApiRequestBodyObject = {
  description?: string;
  required?: boolean;
  content: Record<string, { schema?: OpenApiSchemaObject | OpenApiRefObject }>;
};

type OpenApiOperationObject = {
  operationId?: string;
  summary?: string;
  description?: string;
  parameters?: Array<OpenApiParameterObject | OpenApiRefObject>;
  requestBody?: OpenApiRequestBodyObject | OpenApiRefObject;
  responses?: Record<string, unknown>;
  tags?: string[];
  deprecated?: boolean;
};

type OpenApiPathItem = {
  get?: OpenApiOperationObject;
  post?: OpenApiOperationObject;
  put?: OpenApiOperationObject;
  delete?: OpenApiOperationObject;
  patch?: OpenApiOperationObject;
  parameters?: Array<OpenApiParameterObject | OpenApiRefObject>;
};

type OpenApiSpec = {
  openapi: string;
  info: {
    title: string;
    version: string;
    description?: string;
  };
  servers?: Array<{
    url: string;
    description?: string;
  }>;
  paths: Record<string, OpenApiPathItem>;
  components?: {
    schemas?: Record<string, OpenApiSchemaObject>;
    parameters?: Record<string, OpenApiParameterObject>;
    requestBodies?: Record<string, OpenApiRequestBodyObject>;
  };
};

type OpenApiToolsOptions = {
  baseUrl?: string;
  headers?: Record<string, string>;
  auth?: OpenApiAuth;
  include?: string[];
  exclude?: string[];
  namePrefix?: string;
  operations?: Record<
    string,
    | false
    | {
        include?: boolean;
        name?: string;
        description?: string;
        responseExamples?: Array<{
          status?: string | number;
          description?: string;
          value: unknown;
        }>;
      }
  >;
};

// =============================================================================
// CreateSmithers
// =============================================================================

type CreateSmithersOptions = {
  readableName?: string;
  description?: string;
  alertPolicy?: SmithersAlertPolicy;
  dbPath?: string;
  journalMode?: string;
};

type CreateSmithersPostgresOptions =
  CreateSmithersOptions & (
    | { provider?: "postgres"; connectionString?: string; connection?: object }
    | { provider: "pglite"; dataDir?: string }
  );

// Named export from "smithers-orchestrator". The returned `smithers` property is
// the workflow wrapper; there is no default-exported top-level smithers function.
declare function createSmithers<Schemas extends Record<string, import("zod").ZodObject<any>>>(
  schemas: Schemas,
  opts?: CreateSmithersOptions,
): CreateSmithersApi<Schemas>;

declare function createSmithersPostgres<Schemas extends Record<string, import("zod").ZodObject<any>>>(
  schemas: Schemas,
  opts?: CreateSmithersPostgresOptions,
): Promise<CreateSmithersApi<Schemas> & { close: () => Promise<void> }>;

type SchemaOutput<Schema> = Extract<
  Schema[keyof Schema],
  import("zod").ZodObject<import("zod").ZodRawShape>
>;
type RuntimeSchema<Schema> =
  Schema extends { input: infer Input }
    ? Omit<Schema, "input"> & { input: Input extends import("zod").ZodTypeAny ? import("zod").infer<Input> : Input }
    : Schema;

type CreateSmithersApi<Schema = unknown> = {
  Workflow: (props: WorkflowProps) => React.ReactElement;
  Approval: <Row>(props: ApprovalProps<Row, SchemaOutput<Schema>>) => React.ReactElement;
  Task: <Row, D extends DepsSpec = {}>(props: TaskProps<Row, SchemaOutput<Schema>, D>) => React.ReactElement;
  Sequence: typeof Sequence;
  Parallel: typeof Parallel;
  MergeQueue: typeof MergeQueue;
  Branch: typeof Branch;
  Loop: typeof Loop;
  Ralph: typeof Ralph;
  ContinueAsNew: typeof ContinueAsNew;
  continueAsNew: typeof continueAsNew;
  Worktree: typeof Worktree;
  Sandbox: (props: SandboxProps) => React.ReactElement;
  Signal: <SignalSchema extends import("zod").ZodObject<import("zod").ZodRawShape>>(props: SignalProps<SignalSchema>) => React.ReactElement;
  Timer: typeof Timer;
  useCtx: () => SmithersCtx<RuntimeSchema<Schema>>;
  smithers: (
    build: (ctx: SmithersCtx<RuntimeSchema<Schema>>) => React.ReactElement,
    opts?: SmithersWorkflowOptions,
  ) => SmithersWorkflow<RuntimeSchema<Schema>>;
  db: import("drizzle-orm/bun-sqlite").BunSQLiteDatabase<Record<string, unknown>>;
  tables: { [K in keyof Schema]: unknown };
  outputs: { [K in keyof Schema]: Schema[K] };
};

type SerializedCtx = {
  runId: string;
  iteration: number;
  iterations: Record<string, number>;
  input: unknown;
  outputs: OutputSnapshot;
};

type HostNodeJson =
  | {
      kind: "element";
      tag: string;
      props: Record<string, string>;
      rawProps: Record<string, any>;
      children: HostNodeJson[];
    }
  | {
      kind: "text";
      text: string;
    };

type ExternalSmithersConfig<S extends Record<string, import("zod").ZodObject<any>>> = {
  schemas: S;
  agents: Record<string, AgentLike>;
  buildFn: (ctx: SerializedCtx) => HostNodeJson;
  dbPath?: string;
};

declare function createExternalSmithers<S extends Record<string, import("zod").ZodObject<any>>>(
  config: ExternalSmithersConfig<S>,
): SmithersWorkflow<S> & { tables: Record<string, any>; cleanup: () => void };

// =============================================================================
// Observability (smithers-orchestrator/observability)
// =============================================================================

type SmithersLogFormat = "json" | "pretty";
type SmithersObservabilityService = { emit(event: SmithersEvent): void | Promise<void> };
type SmithersObservabilityOptions = { service?: SmithersObservabilityService; logFormat?: SmithersLogFormat };
type ResolvedSmithersObservabilityOptions = SmithersObservabilityOptions & { metricsPort?: number; metricsPath?: string };
```

For canonical, machine-checked types, install `smithers-orchestrator` and use editor go-to-definition. For runtime errors, see Errors.

---

## Error Reference

> Exhaustive Smithers error codes, typed error helpers, and HTTP API error responses.

`SmithersErrorInstance` is a typed, code-bearing `Error` subclass used throughout Smithers internals. It surfaces when `runWorkflow` throws, in `NodeFailed` events emitted during execution, and as JSON in HTTP API error responses. The imports below are the full error utility surface.

<Note>
**`build agent command: undefined is not an object (evaluating 'schema._zod.def')`**:
your task `output` schema is a **Zod v3** object. Smithers reads schema metadata
via Zod v4 internals (`_zod.def`). Install Zod v4 (`bun add zod@^4`) and import
`z` from it. This is a deterministic configuration error; re-running will not fix
it.
</Note>

```ts
import {
  ERROR_REFERENCE_URL,
  SmithersErrorInstance,
  errorToJson,
  getSmithersErrorDefinition,
  getSmithersErrorDocsUrl,
  isKnownSmithersErrorCode,
  isSmithersError,
  knownSmithersErrorCodes,
} from "smithers-orchestrator";
import type {
  KnownSmithersErrorCode,
  SmithersError,
  SmithersErrorCode,
} from "smithers-orchestrator";
```

Every built-in `SmithersErrorInstance` carries three pieces of documentation metadata:

| Field | Meaning |
|---|---|
| `message` | Human-readable description followed by a docs URL, e.g. `"Input failed validation. See https://…"` |
| `summary` | Raw message without the docs suffix. |
| `docsUrl` | Reference URL for Smithers errors. |

Use `KnownSmithersErrorCode` for an exhaustive switch over built-in Smithers codes. `SmithersErrorCode` includes the `(string & {})` escape hatch for user-defined custom codes.

| Export | Kind | Description |
|---|---|---|
| `SmithersErrorInstance` | class | Runtime error class used throughout Smithers internals. |
| `isSmithersError(err)` | function | Type guard for values carrying a Smithers-style `code`. |
| `isKnownSmithersErrorCode(code)` | function | Narrows a string to the built-in exhaustive error-code union. |
| `knownSmithersErrorCodes` | value | Array of every built-in Smithers error code documented on this page. |
| `getSmithersErrorDocsUrl(code)` | function | Returns the docs URL appended to built-in error messages. |
| `getSmithersErrorDefinition(code)` | function | Returns category, description, and details metadata for known codes. |
| `errorToJson(err)` | function | Serializes `name`, `message`, `summary`, `docsUrl`, `code`, `details`, `cause`, and `stack`. |
| `ERROR_REFERENCE_URL` | value | Base docs URL for Smithers runtime errors. |
| `KnownSmithersErrorCode` | type | Exact built-in Smithers code union. |
| `SmithersErrorCode` | type | Built-in codes plus the custom string escape hatch. |
| `SmithersError` | type | Public typed shape for serialized Smithers errors. |

```ts
import { Effect } from "effect";
import { runWorkflow } from "smithers-orchestrator";

try {
  await Effect.runPromise(runWorkflow(workflow, { input: {} }));
} catch (err) {
  if (isSmithersError(err) && isKnownSmithersErrorCode(err.code)) {
    switch (err.code) {
      case "INVALID_INPUT":
        console.error("Bad input:", err.summary);
        break;
      case "AGENT_CLI_ERROR":
        console.error("Agent failed:", err.summary);
        break;
      default:
        console.error(`[${err.code}] ${err.summary}`);
    }

    console.error("Docs:", err.docsUrl);
  }
}
```

## Engine

| Code | When | Details |
|---|---|---|
| `INVALID_INPUT` | Workflow input fails validation or the runtime receives a non-object input payload. | -- |
| `MISSING_INPUT` | A resume run references an input row that is missing from the database. | -- |
| `MISSING_INPUT_TABLE` | The workflow schema does not expose the expected input table during resume or hydration. | -- |
| `RESUME_METADATA_MISMATCH` | Stored run metadata no longer matches the workflow being resumed. Editing the workflow file or an imported module between stop and resume triggers this (resume hashes file content, not git, so no commit is required). Fork/replay onto the edit or start a fresh run; revert the file to resume the original run. | `mismatches`, `existing`, `current` |
| `UNKNOWN_OUTPUT_SCHEMA` | A task references an output table that is not present in the schema registry. | -- |
| `INVALID_OUTPUT` | Agent output cannot be parsed or validated against the declared output schema. | -- |
| `WORKTREE_CREATE_FAILED` | Smithers fails to create or hydrate a git or jj worktree for a task. | `{ worktreePath, vcsType, branch? }` |
| `VCS_NOT_FOUND` | No supported git or jj repository root can be found for the workflow. | `{ rootDir }` |
| `SNAPSHOT_NOT_FOUND` | A requested time-travel snapshot or frame does not exist. | `{ runId, frameNo }` |
| `VCS_WORKSPACE_CREATE_FAILED` | Smithers fails to materialize a jj workspace for time-travel or replay. | `{ runId, frameNo, vcsPointer, workspacePath }` |
| `TASK_EMPTY_PROMPT` | A `<Task>` prompt renders to an empty string, so the agent would be invoked with no input. | `{ nodeId, iteration }` |
| `WORKFLOW_RENDER_FAILED` | The workflow component throws while rendering the graph (e.g. a hook called outside a component render, or a bug in the workflow function). | `{ workflowPath }` |
| `TASK_TIMEOUT` | A task compute callback exceeds its configured timeout. | `{ nodeId, attempt, timeoutMs }` |
| `TASK_HIJACK_UNSUPPORTED` | A task requests auto-hijack but its agent cannot provide a resumable session or conversation. | `{ nodeId, agentId? }` |
| `TASK_FORK_SOURCE_NOT_COMPLETE` | A forked task began executing but its fork source has not completed, so no session snapshot exists yet. | `{ nodeId, forkSource }` |
| `TASK_FORK_SESSION_UNAVAILABLE` | A `<Task fork>` cannot obtain a usable agent session snapshot, either because the forking task is not an agent task or because the source completed without producing a forkable conversation (e.g. a compute/static, skipped, or cancelled source). | `{ nodeId, forkSource }` |
| `TASK_ABORTED` | A running task is aborted through an AbortSignal or shutdown path. | -- |
| `RUN_NOT_FOUND` | A CLI or engine command references a run ID that does not exist in the database. | `{ runId }` |
| `NODE_NOT_FOUND` | A CLI command references a node ID that does not exist for the given run. | `{ runId, nodeId }` |
| `SANDBOX_BUNDLE_INVALID` | A sandbox bundle fails validation (missing README, invalid manifest, etc.). | `{ bundlePath }` |
| `SANDBOX_BUNDLE_TOO_LARGE` | A sandbox bundle exceeds the maximum allowed size. | `{ bundlePath, maxBytes }` |
| `WORKFLOW_EXECUTION_FAILED` | A child or builder workflow exits unsuccessfully without surfacing a typed error payload. | `{ status }` |
| `SANDBOX_EXECUTION_FAILED` | Sandbox setup or execution fails before a more specific sandbox error can be emitted. | `{ sandboxId, runId?, maxConcurrent?, activeSandboxCount? }` |
| `TASK_HEARTBEAT_TIMEOUT` | A task heartbeat timeout is exceeded while the task is still in progress. | `{ nodeId, iteration, attempt, timeoutMs, staleForMs, lastHeartbeatAtMs }` |
| `HEARTBEAT_PAYLOAD_TOO_LARGE` | A task heartbeat payload exceeds the maximum persisted checkpoint size. | `{ dataSizeBytes, maxBytes }` |
| `HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE` | A task heartbeat payload contains values that cannot be serialized to JSON. | `{ path, valueType? }` |
| `RUN_CANCELLED` | A run is cancelled while runtime work is still active. | `{ runId }` |
| `RUN_NOT_RESUMABLE` | A resume request targets a run state that cannot be resumed. | `{ runId, status }` |
| `RUN_OWNER_ALIVE` | A resume attempt is skipped because the process that started the run is still alive (heartbeating). This is normal; it prevents two processes from running the same workflow simultaneously. | `{ runId, runtimeOwnerId }` |
| `RUN_STILL_RUNNING` | A recovery or resume operation finds a run that is still active. | `{ runId }` |
| `RUN_RESUME_CLAIM_LOST` | A runtime loses the resume claim before it can update the run. | `{ runId, runtimeOwnerId }` |
| `RUN_RESUME_CLAIM_FAILED` | A runtime cannot claim a stale run for resume. | `{ runId, runtimeOwnerId }` |
| `RUN_RESUME_ACTIVATION_FAILED` | A claimed run cannot be moved back into active execution. | `{ runId, runtimeOwnerId }` |
| `RUN_HIJACKED` | A run is interrupted because another runtime hijacked execution. | `{ runId, hijackTarget }` |
| `CONTINUATION_STATE_TOO_LARGE` | Continue-as-new state exceeds the configured serialized size limit. | `{ runId, sizeBytes, maxBytes }` |
| `INVALID_CONTINUATION_STATE` | Continue-as-new state cannot be parsed or applied. | -- |
| `RALPH_MAX_REACHED` | A Ralph loop reaches maxIterations with fail-on-max behavior. | `{ ralphId, maxIterations }` |
| `SCHEDULER_ERROR` | The scheduler cannot produce a valid execution decision. | -- |
| `SESSION_ERROR` | The workflow session state machine reaches an invalid or failed state. | -- |

## Components

| Code | When | Details |
|---|---|---|
| `TASK_ID_REQUIRED` | `<Task>` is missing a valid string id. | -- |
| `TASK_MISSING_OUTPUT` | `<Task>` is missing its output prop. | `{ nodeId }` |
| `TASK_FORK_SOURCE_NOT_FOUND` | A `<Task fork>` references a source task id that is not present in the workflow graph, including a source that exists only in an unselected branch. | `{ nodeId, forkSource }` |
| `TASK_FORK_CYCLE` | A `<Task fork>` introduces a dependency cycle, directly or indirectly. | `{ nodeId, forkSource }` |
| `DUPLICATE_ID` | Two nodes with the same runtime id are mounted in one workflow graph. | `{ kind, id }` |
| `NESTED_LOOP` | `<Loop>` or `<Ralph>` is nested inside another loop construct that Smithers does not support. | -- |
| `WORKTREE_EMPTY_PATH` | `<Worktree>` is mounted with an empty path. | -- |
| `MDX_PRELOAD_INACTIVE` | A prompt object is rendered without the MDX preload layer being active. | -- |
| `CONTEXT_OUTSIDE_WORKFLOW` | Workflow context access happens outside an active Smithers workflow render. | -- |
| `MISSING_OUTPUT` | Code calls `ctx.output()` for a node result that does not exist. | `{ nodeId, iteration }` |
| `DEP_NOT_SATISFIED` | A typed dep on `<Task>` references an upstream output that has not been produced yet. | `{ taskId, depKey, resolvedNodeId }` |
| `ASPECT_BUDGET_EXCEEDED` | An Aspects budget (tokens or latency) has been exceeded. | `{ kind, limit, current }` |
| `APPROVAL_OUTSIDE_TASK` | `<Approval>` is resolved outside the active task runtime. | -- |
| `APPROVAL_OPTIONS_REQUIRED` | An approval mode that requires explicit options is missing them. | -- |
| `WORKFLOW_MISSING_DEFAULT` | A workflow module does not export a default Smithers workflow. | -- |
| `WORKFLOW_NOT_BUILT` | A workflow's default export is a raw component or JSX element instead of the object returned by `smithers(...)`. | -- |

## Tools

| Code | When | Details |
|---|---|---|
| `TOOL_PATH_INVALID` | A filesystem tool receives a non-string path. | -- |
| `TOOL_PATH_ESCAPE` | A filesystem tool resolves a path outside the sandbox root, including through symlinks. | -- |
| `TOOL_FILE_TOO_LARGE` | A read or edit operation exceeds the configured file size limit. | -- |
| `TOOL_CONTENT_TOO_LARGE` | A write operation exceeds the configured content size limit. | -- |
| `TOOL_PATCH_TOO_LARGE` | An edit patch exceeds the configured patch size limit. | -- |
| `TOOL_PATCH_FAILED` | A unified diff patch cannot be applied to the target file. | -- |
| `TOOL_NETWORK_DISABLED` | The bash tool tries to access the network while network access is disabled. | -- |
| `TOOL_GIT_REMOTE_DISABLED` | The bash tool attempts a remote git operation while network access is disabled. | -- |
| `TOOL_COMMAND_FAILED` | A bash tool command exits with a non-zero status. | -- |
| `TOOL_GREP_FAILED` | The grep tool fails with an rg execution error. | -- |

## Agents

| Code | When | Details |
|---|---|---|
| `AGENT_CLI_ERROR` | A CLI-backed agent exits unsuccessfully, streams an explicit error, or its RPC transport fails. | -- |
| `AGENT_QUOTA_EXCEEDED` | An agent provider returns a usage-limit or quota error. The failure is transient; retries are preserved and the run pauses until the reset time. | `{ agentId?, agentEngine?, agentModel?, quotaResetAtMs?, resetHint? }` |
| `AGENT_CONFIG_INVALID` | A CLI-backed agent fails with a non-retryable configuration error such as an unknown model, missing LLM, or unsupported model. | -- |
| `AGENT_RPC_FILE_ARGS` | Pi RPC mode is used with file arguments that the transport does not support. | -- |
| `AGENT_BUILD_COMMAND` | An agent implementation forbids `buildCommand()` because it uses a custom `generate()` transport. | -- |
| `AGENT_DIAGNOSTIC_TIMEOUT` | An internal agent diagnostic check exceeds the per-check timeout budget. | -- |

## Database

| Code | When | Details |
|---|---|---|
| `DB_MISSING_COLUMNS` | A table used by Smithers does not expose required columns such as `runId` or `nodeId`. | -- |
| `DB_REQUIRES_BUN_SQLITE` | The database adapter is not backed by a Bun SQLite client with `exec()`. | -- |
| `DB_QUERY_FAILED` | A database read query throws or rejects while running inside an Effect. | -- |
| `DB_WRITE_FAILED` | A database write or migration fails, including after SQLite retry exhaustion. | -- |
| `SMITHERS_BACKEND_CONFLICT` | Multiple Smithers backend stores contain run history and no `migrated.json` receipt explains the divergence. | `{ populatedBackends, stores }` |
| `SMITHERS_MIGRATION_REQUIRED` | A physical Smithers store has run data but the resolved backend points at another store, so the history would be invisible until you migrate or pin the existing backend. | `{ sourceBackend, targetBackend, dbPath?, location?, runCount, schemaVersion, resolvedBackend? }` |
| `STORAGE_ERROR` | A storage service operation fails before surfacing a more specific database code. | -- |

### Migration errors

`smithers migrate` preserves the source SQLite store by default. If the legacy
`smithers.db` cannot be copied into the target backend, Smithers leaves the
original file untouched and reports the first actionable failure.

Corrupt, malformed, encrypted, or non-SQLite source files surface as
`DB_QUERY_FAILED` with the source `dbPath` in `details`. The error message tells
the operator to verify the file with `sqlite3 <dbPath> 'PRAGMA integrity_check'`
and restore from backup or start fresh if SQLite confirms corruption.

Source files that exist but cannot be opened also surface as `DB_QUERY_FAILED`.
That message points at the common operational causes: another process holding the
file, unreadable permissions, or a copied SQLite file missing its
`smithers.db-wal` / `smithers.db-shm` sidecars.

For Postgres migrations, `smithers migrate --to postgres` validates the target
connection string before opening the source store. Missing `--url`,
`SMITHERS_POSTGRES_URL`, or `DATABASE_URL` fails fast with `INVALID_INPUT` so
connection setup problems are not hidden behind unrelated source-store errors.

## Effect / Runtime

| Code | When | Details |
|---|---|---|
| `INTERNAL_ERROR` | An unexpected internal exception crossed an Effect boundary without a more specific Smithers code. | -- |
| `PROCESS_ABORTED` | A spawned child process is aborted by signal or shutdown. | `{ command, args, cwd }` |
| `PROCESS_TIMEOUT` | A spawned child process exceeds its total timeout. | `{ command, args, cwd, timeoutMs }` |
| `PROCESS_IDLE_TIMEOUT` | A spawned child process stops producing output longer than its idle timeout. | `{ command, args, cwd, idleTimeoutMs }` |
| `PROCESS_SPAWN_FAILED` | The runtime cannot spawn the requested child process. | `{ command, args, cwd }` |
| `TASK_RUNTIME_UNAVAILABLE` | Builder task runtime APIs are accessed outside an executing step. | -- |

## Hot Reload

| Code | When | Details |
|---|---|---|
| `SCHEMA_CHANGE_HOT` | Hot reload detects a schema change that requires a full restart. | -- |
| `HOT_OVERLAY_FAILED` | Building or cleaning the generated hot-reload overlay fails. | -- |
| `HOT_RELOAD_INVALID_MODULE` | A hot-reloaded workflow module does not export a valid default workflow build. | -- |

## Scorers

| Code | When | Details |
|---|---|---|
| `SCORER_FAILED` | A scorer throws or rejects while Smithers is evaluating a result. | -- |

## CLI

| Code | When | Details |
|---|---|---|
| `INVALID_EVENTS_OPTIONS` | The smithers events command receives invalid filter options. | -- |
| `WORKFLOW_EXISTS` | The workflow creation CLI refuses to overwrite an existing workflow file. | -- |
| `CLI_DB_NOT_FOUND` | A CLI command cannot find a nearby `smithers.db` file. | -- |
| `CLI_AGENT_UNSUPPORTED` | The ask command selects an agent integration that Smithers does not support in that mode. | -- |

## Integrations

| Code | When | Details |
|---|---|---|
| `PI_HTTP_ERROR` | The Pi or server integration receives a non-success HTTP response from Smithers. | -- |
| `EXTERNAL_BUILD_FAILED` | An external workflow host fails to build a Smithers HostNode payload. | `{ scriptPath, error?, exitCode?, stderr?, stdout? }` |
| `SCHEMA_DISCOVERY_FAILED` | External workflow schema discovery fails or returns invalid output. | `{ scriptPath, error?, exitCode?, stderr? }` |
| `OPENAPI_SPEC_LOAD_FAILED` | An OpenAPI spec cannot be loaded or parsed. | -- |
| `OPENAPI_OPERATION_NOT_FOUND` | The requested operationId does not exist in the OpenAPI spec. | -- |
| `OPENAPI_TOOL_EXECUTION_FAILED` | An OpenAPI tool call fails during HTTP execution. | -- |
| `ACCOUNT_INVALID` | An account entry, label, provider, or provider-specific configuration is invalid. | -- |
| `ACCOUNT_NOT_FOUND` | An account operation references a label that is not registered. | -- |
| `ACCOUNT_DUPLICATE_LABEL` | An account add operation would create a duplicate label without replace enabled. | -- |
| `ACCOUNTS_FILE_INVALID` | The accounts.json file is not valid JSON or does not match the expected account registry schema after tolerant entry filtering. | -- |

Unknown legacy account providers are tolerated entry-by-entry. For example, an
old `"provider": "gemini"` subscription is skipped with a warning that names the
account label and valid providers, while the remaining valid accounts still
load. Such an entry is left out of the active account list but preserved
verbatim in `accounts.json` across later `agents add` and `agents remove` calls,
so an unrelated change never destroys the credentials its `configDir` points at.
Run `bunx smithers-orchestrator agents remove <label>` to delete it, or
`bunx smithers-orchestrator agents add --label <label> --replace ...` to migrate
it onto a supported provider.
`ACCOUNTS_FILE_INVALID` is reserved for invalid JSON or entries whose
known provider shape is malformed.

## HTTP API Errors

JSON response codes, not `SmithersErrorInstance` objects.

| Code | Status | When |
|---|---|---|
| `INVALID_REQUEST` | 400 | Invalid request body or query params |
| `PAYLOAD_TOO_LARGE` | 413 | Body exceeds `maxBodyBytes` |
| `INVALID_JSON` | 400 | Body not valid JSON |
| `SERVER_ERROR` | 500 | Unexpected server error |
| `UNAUTHORIZED` | 401 | Missing or invalid auth token |
| `WORKFLOW_PATH_OUTSIDE_ROOT` | 400 | Workflow path outside server root |
| `RUN_ID_REQUIRED` | 400 | `runId` required when `resume: true` |
| `RUN_ALREADY_EXISTS` | 409 | Run ID already exists |
| `RUN_NOT_FOUND` | 404 | No run with given ID |
| `RUN_NOT_ACTIVE` | 409 | Run not active (cannot cancel) |
| `NOT_FOUND` | 404 | Route or resource not found |
| `DB_NOT_CONFIGURED` | 400 | Server database not configured |


---

## Package Configuration

> Reference for the smithers-orchestrator package exports, TypeScript configuration, and Bun preload setup.

This page covers: CLI binary usage, subpath export map, TypeScript compiler options, Bun preload and test config, and npm scripts.

## Binary

Use `bunx smithers-orchestrator <command>` for CLI commands. The repository root keeps a private development bin that points at `apps/cli/src/index.js`; application code should import from the package exports below.

## Subpath Exports

Use the subpath form to import only the surface you need. Entry files in this table are relative to the published `smithers-orchestrator` package; in the repository they live under `packages/smithers/`.

| Import path | Entry file | Purpose |
|---|---|---|
| `smithers-orchestrator` | `./src/index.js` | Core API: `createSmithers`, `openSmithersBackend`, components, `runWorkflow`, `renderMdx`, errors |
| `smithers-orchestrator/gateway` | `./src/gateway.js` | Gateway server primitives from `@smithers-orchestrator/server/gateway` |
| `smithers-orchestrator/gateway-client` | `./src/gateway-client.js` | Typed client helpers from `@smithers-orchestrator/gateway-client` |
| `smithers-orchestrator/gateway-react` | `./src/gateway-react.js` | React hooks and providers for gateway-backed UIs |
| `smithers-orchestrator/gateway-ui` | `./src/gateway-ui.js` | Prebuilt React components for gateway-backed UIs (`RunList`, `RunTree`, `ApprovalPanel`, and more) |
| `smithers-orchestrator/ui` | `./src/ui.js` | Shared shadcn-anatomy component library (`Button`, `Card`, `Dialog`, `Tabs`, `StatusPill`, and more) styled through the ui-styleguide theme tokens |
| `smithers-orchestrator/sandbox` | `./src/sandbox.js` | Sandbox provider contracts, bundles, and execution helpers |
| `smithers-orchestrator/telegram` | `./src/telegram.js` | Telegram Bot API helpers for serverless bots: webhook secret verification, update normalization, typed Bot API calls, retries, MarkdownV2 escaping, chunking, and test fakes |
| `smithers-orchestrator/cloudflare` | `./src/cloudflare.js` | Cloudflare Workers helpers: Sandbox provider, Durable Object SQLite descriptor, and D1 descriptor |
| `smithers-orchestrator/daytona` | `./src/daytona.js` | Daytona sandbox provider: run child workflows in Daytona workspaces |
| `smithers-orchestrator/vercel` | `./src/vercel.js` | Vercel Sandbox provider: run child workflows in Vercel sandboxes |
| `smithers-orchestrator/aws` | `./src/aws.js` | AWS sandbox provider: run child workflows on ECS Fargate or CodeBuild with S3 bundle transport |
| `smithers-orchestrator/gcp` | `./src/gcp.js` | GCP sandbox provider: run child workflows on Cloud Run Jobs with GCS bundle transport |
| `smithers-orchestrator/jsx-runtime` | `./src/jsx-runtime.js` | JSX runtime (auto-resolved by `jsxImportSource`) |
| `smithers-orchestrator/jsx-dev-runtime` | `./src/jsx-runtime.js` | JSX dev runtime (auto-resolved in dev mode) |
| `smithers-orchestrator/tools` | `./src/tools.js` | Tool sandbox: `defineTool`, `read`, `grep`, `bash`, `edit`, `write` |
| `smithers-orchestrator/testing` | `./src/testing.js` | Workflow test helpers: `fakeAgent`, `renderWorkflow`, `renderPrompt`, and `runTask` |
| `smithers-orchestrator/server` | `./src/server.js` | HTTP server for run management and event streaming |
| `smithers-orchestrator/observability` | `./src/observability.js` | OpenTelemetry traces, metrics, and Prometheus integration |
| `smithers-orchestrator/mdx-plugin` | `./src/mdx-plugin.js` | Bun preload plugin for `.mdx` imports |
| `smithers-orchestrator/dom/renderer` | `./src/dom/renderer.js` | Internal renderer (advanced use) |
| `smithers-orchestrator/serve` | `./src/serve.js` | Single-workflow HTTP server via `createServeApp` |
| `smithers-orchestrator/scorers` | `./src/scorers.js` | Eval scorers: `createScorer`, `llmJudge`, `aggregateScores` |
| `smithers-orchestrator/memory` | `./src/memory.js` | Cross-run facts, message history, processors, and metrics |
| `smithers-orchestrator/openapi` | `./src/openapi.js` | Generate AI SDK tools from OpenAPI specs |
| `smithers-orchestrator/control-plane` | `./src/control-plane.js` | Organization, project, billing, usage, secret-reference, and audit primitives |
| `smithers-orchestrator/*` | `./src/*.js` | Wildcard-backed facade wrappers for additional public subpaths |

The PI plugin is published as the separate `@smithers-orchestrator/pi-plugin` package. The old `smithers-orchestrator/pi-plugin` and `smithers-orchestrator/pi-extension` subpaths are no longer exported.

## Workspace Packages

Most applications should import from `smithers-orchestrator`. The workspace packages below are listed for advanced integrations, custom clients, framework development, and monorepo orientation. Some app workspaces are private and are not published packages.

| Package | Primary surface | Related docs |
|---|---|---|
| `smithers-orchestrator` | Public facade for workflow authoring, components, agents, tools, server helpers, memory, OpenAPI tools, scorers, and JSX runtime setup | This page, Types |
| `@smithers-orchestrator/cli` | CLI entrypoint, MCP server, local workflow pack, account registry commands, DevTools commands, cron, alerts, and server commands | CLI Overview, MCP Server |
| `@smithers-orchestrator/cloudflare` | Cloudflare Workers integration: Sandbox SDK provider, Durable Object SQLite descriptor, and D1 descriptor | Sandbox, Server |
| `@smithers-orchestrator/aws` | AWS sandbox provider: ECS Fargate and CodeBuild execution with S3 bundle transport | Sandbox, AWS |
| `@smithers-orchestrator/daytona` | Daytona sandbox provider: run child workflows in Daytona workspaces | Sandbox, Daytona |
| `@smithers-orchestrator/gcp` | GCP sandbox provider: Cloud Run Jobs execution with GCS bundle transport | Sandbox, GCP |
| `@smithers-orchestrator/vercel` | Vercel Sandbox provider: run child workflows in Vercel sandboxes | Sandbox, Vercel |
| `@smithers-orchestrator/accounts` | Subscription and API-key account registry helpers: `listAccounts`, `addAccount`, `removeAccount`, `getAccount`, `accountToProviderEnv` | CLI Agents |
| `@smithers-orchestrator/agent-eliza` | elizaOS `AgentRuntime` harness for Smithers; opt-in package that owns the `@elizaos/core` dependency | SDK Agents |
| `@smithers-orchestrator/agents` | AI SDK and CLI agent adapters, CLI capability reports, agent contracts, and tool capability registry | CLI Agents, SDK Agents |
| `@smithers-orchestrator/automate-site` | Private Cloudflare Worker deployment for the event-triggered durable workflows marketing site | Ecosystem |
| `@smithers-orchestrator/bug-worker` | Private Cloudflare Worker behind `bug.smithers.sh` that receives `smithers bug` reports and stores them in KV | CLI Overview |
| `@smithers-orchestrator/components` | JSX workflow components such as `Task`, `Workflow`, `Approval`, `Sandbox`, `Timer`, `Signal`, and control-flow components | Components |
| `@smithers-orchestrator/control-plane` | Durable organization, project, team, billing, usage, secret-reference, and audit primitives for hosted deployments | Control Plane |
| `@smithers-orchestrator/db` | SQLite/Drizzle adapter, table setup, run-state derivation, schema helpers, and output tables | Data Model, Run State |
| `@smithers-orchestrator/ddd-site` | Private Cloudflare Worker deployment for the Docs-Driven Development marketing site | Ecosystem |
| `@smithers-orchestrator/devtools` | Snapshot, tree, diff, node lookup, task collection, and DevTools run-store helpers behind the CLI inspect commands | Debugging, CLI Overview |
| `@smithers-orchestrator/driver` | Runtime driver contracts: `RunOptions`, `RunResult`, `RunStatus`, `SmithersCtx`, outputs, task runtime, interop, and child process helpers | Run Workflow, Execution Model |
| `@smithers-orchestrator/electric-proxy` | ElectricSQL shape proxy for cloud sync, scoped shape access, rate limits, stripped auth forwarding, and shape metrics | Production Hardening, Gateway |
| `@smithers-orchestrator/engine` | Workflow rendering/execution API: `runWorkflow`, `renderFrame`, `workflow`, `Smithers`, `fragment`, signals, and Effect versioning | Render Frame, Run Workflow |
| `@smithers-orchestrator/errors` | Error definitions, known codes, JSON serialization, docs URLs, and type guards | Errors |
| `@smithers-orchestrator/gateway` | Stable Gateway RPC contracts, auth scopes, deployment metadata, and generated OpenAPI schema | Gateway, RPC |
| `@smithers-orchestrator/gateway-client` | Browser/client SDK for Gateway RPC requests and event streams | Gateway |
| `@smithers-orchestrator/gateway-react` | React hooks and root helpers for Gateway-backed UIs | Gateway |
| `@smithers-orchestrator/gateway-ui` | Prebuilt React components for Gateway-backed UIs, built on the gateway-react hooks | Gateway UI Components |
| `@smithers-orchestrator/graph` | Framework-neutral workflow graph model, XML nodes, task descriptors, and graph snapshots | Planner Internals, Types |
| `@smithers-orchestrator/init-site` | Private Cloudflare Worker deployment for the interactive Smithers init wizard and workflow builder marketing site | Ecosystem |
| `@smithers-orchestrator/integrations` | External webhook and polling event sources, cursor storage, signature verification, and delivery helpers for workflow integrations | Integrations, Server |
| `@smithers-orchestrator/jj-darwin-arm64` | Vendored jj (Jujutsu) binary for darwin-arm64; auto-installed as an optional dependency of `@smithers-orchestrator/vcs`, not depended on directly | VCS Guide |
| `@smithers-orchestrator/jj-darwin-x64` | Vendored jj (Jujutsu) binary for darwin-x64; auto-installed as an optional dependency of `@smithers-orchestrator/vcs`, not depended on directly | VCS Guide |
| `@smithers-orchestrator/jj-linux-arm64` | Vendored jj (Jujutsu) binary for linux-arm64; auto-installed as an optional dependency of `@smithers-orchestrator/vcs`, not depended on directly | VCS Guide |
| `@smithers-orchestrator/jj-linux-x64` | Vendored jj (Jujutsu) binary for linux-x64; auto-installed as an optional dependency of `@smithers-orchestrator/vcs`, not depended on directly | VCS Guide |
| `@smithers-orchestrator/jj-win32-x64` | Vendored jj (Jujutsu) binary for win32-x64; auto-installed as an optional dependency of `@smithers-orchestrator/vcs`, not depended on directly | VCS Guide |
| `@smithers-orchestrator/memory` | Cross-run facts, message history, processors, namespaces, service layer, and metrics | Memory, Memory Quickstart |
| `@smithers-orchestrator/monitor-site` | Private Cloudflare Worker deployment for the full-screen Smithers TUI monitor marketing site | Ecosystem |
| `@smithers-orchestrator/observability` | Event types, logging, tracing, metrics, Prometheus rendering, and runtime observability layers | Events, Event Types |
| `@smithers-orchestrator/openapi` | OpenAPI parsing, operation extraction, AI SDK tool generation, schema conversion, and metrics | OpenAPI Tools, Tools |
| `@smithers-orchestrator/automate-site` | Private Cloudflare Worker deployment for the automation marketing site | Ecosystem |
| `@smithers-orchestrator/ddd-site` | Private Cloudflare Worker deployment for the docs-driven-development marketing site | Ecosystem |
| `@smithers-orchestrator/init-site` | Private Cloudflare Worker deployment for the init marketing site | Ecosystem |
| `@smithers-orchestrator/monitor-site` | Private Cloudflare Worker deployment for the monitor marketing site | Ecosystem |
| `@smithers-orchestrator/openclaw-site` | Private Cloudflare Worker deployment for the OpenClaw marketing site | Ecosystem |
| `@smithers-orchestrator/plugins-site` | Private Cloudflare Worker deployment for the plugins marketing site | Ecosystem |
| `@smithers-orchestrator/self-healing-site` | Private Cloudflare Worker deployment for the self-healing marketing site | Ecosystem |
| `@smithers-orchestrator/telegram-site` | Private Cloudflare Worker deployment for the Telegram marketing site | Ecosystem |
| `@smithers-orchestrator/ui-site` | Private Cloudflare Worker deployment for the UI marketing site | Ecosystem |
| `@smithers-orchestrator/pi-plugin` | PI extension runtime, views, API wrappers, and workflow inspection integration | PI Integration |
| `@smithers-orchestrator/plugins-site` | Private Cloudflare Worker deployment for the Smithers plugins marketing site (Claude Code and Codex) | Ecosystem |
| `@smithers-orchestrator/protocol` | Shared contracts, small value types, and protocol-level errors for cross-package use | Types, Errors |
| `@smithers-orchestrator/react-reconciler` | Custom React reconciler, host context, DOM renderer, DevTools preload, driver, and JSX runtime internals | Why React, Render Frame |
| `@smithers-orchestrator/review` | Review CLI package for code review, PR posting, and story-form HTML walkthrough generation, exposed through `bunx smithers-orchestrator review` and the `smithers-review` bin | CLI Overview |
| `@smithers-orchestrator/sandbox` | Sandbox bundle, execute, and transport primitives used by the `Sandbox` component | Sandbox |
| `@smithers-orchestrator/scheduler` | Pure workflow state machine: task state, scheduler decisions, retry/cache policies, wait reasons, and workflow session services | Workflow State, Suspend and Resume |
| `@smithers-orchestrator/scorers` | Scorer definitions, LLM judges, batch execution, aggregation, persistence schema, and metrics | Evals, Evals Quickstart |
| `@smithers-orchestrator/self-healing-site` | Private Cloudflare Worker deployment for the self-healing agent runs marketing site | Ecosystem |
| `@smithers-orchestrator/server` | HTTP, WebSocket, Gateway, cron, webhook, metrics, and single-workflow serving APIs | Server, Serve |
| `@smithers-orchestrator/smithers-ui` | Private local-only UI app: the full run/approval/memory control surface, served by `bunx smithers-orchestrator ui --app` against a local Gateway | Gateway UI Components |
| `@smithers-orchestrator/telegram` | Telegram Bot API helpers for serverless bots: webhook secret verification, update normalization, typed Bot API calls, retries, MarkdownV2 escaping, chunking, and test fakes | Telegram |
| `@smithers-orchestrator/telegram-site` | Private Cloudflare Worker deployment for the Smithers on Telegram marketing site, including the reference approval Mini App | Telegram, Ecosystem |
| `@smithers-orchestrator/telegram-summary` | Private Cloudflare app: Telegram Bot API ingestion, Kimi daily digests, D1 storage, and the `telegram-summary.smithers.sh` dashboard | Custom Workflow UI, CLI Overview |
| `@smithers-orchestrator/testing` | Source-shipped workflow test helpers for fake agents, frame rendering, prompt rendering, and descriptor task execution | This page, Render Frame |
| `@smithers-orchestrator/time-travel` | Snapshots, diffs, forks, replay, timelines, VCS tags, rewind locks/audits, and time-travel metrics | Time Travel, Time Travel Quickstart |
| `@smithers-orchestrator/tool-context` | AsyncLocalStorage tool-execution context (run/node/idempotency keys, durability snapshot hook) shared by the engine and `smithers-orchestrator` without a dependency cycle | Execution Model |
| `@smithers-orchestrator/tui` | Full-screen OpenTUI + React single-run monitor launched by `bunx smithers-orchestrator up --interactive`; reuses the gateway-react hooks for live tree, graph, logs, timeline, and hijack views of one run | CLI Overview |
| `@smithers-orchestrator/ui-site` | Private Cloudflare Worker deployment for the local Smithers app and AI concierge marketing site | Ecosystem |
| `@smithers-orchestrator/ui` | Shared shadcn-anatomy component library for Smithers UIs: Radix behavior plus CVA variant APIs, styled through the ui-styleguide theme tokens as CSS-in-TS strings | Custom Workflow UI |
| `@smithers-orchestrator/ui-styleguide` | Shared CSS tokens and layout primitives for Smithers browser UIs, including the operator console and workflow UI components | Gateway UI Components, Custom Workflow UI |
| `@smithers-orchestrator/usage` | Account quota and rate-limit usage reporting for Smithers providers | CLI Overview |
| `@smithers-orchestrator/vcs` | VCS discovery and jj workspace operations such as `runJj`, `workspaceAdd`, `workspaceList`, and pointer reverts | VCS Helpers, VCS Guide |

### Usage

```ts
// Core API
import { createSmithers, openSmithersBackend, runWorkflow } from "smithers-orchestrator";

// Tools
import { defineTool, bash, read, write } from "smithers-orchestrator/tools";

// Workflow tests
import { fakeAgent, renderWorkflow } from "smithers-orchestrator/testing";

// Scorers
import { createScorer, llmJudge } from "smithers-orchestrator/scorers";

// MDX plugin (in preload.js)
import { mdxPlugin } from "smithers-orchestrator/mdx-plugin";

// Control-plane primitives
import { ControlPlaneStore } from "smithers-orchestrator/control-plane";
```

## TypeScript Configuration

### JSX Import Source

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "smithers-orchestrator"
  }
}
```

This tells TypeScript to resolve JSX transforms from `smithers-orchestrator/jsx-runtime` instead of `react/jsx-runtime`. The Smithers JSX runtime re-exports React's runtime, so component behavior is identical. This setting enables proper type resolution for Smithers workflow components.

See JSX Installation for the complete TypeScript setup.

### Path Aliases

When developing inside the `smithers-orchestrator` monorepo, the root `tsconfig.json` defines path aliases so source imports resolve without a build step:

```jsonc
"paths": {
  "smithers-orchestrator": ["./packages/smithers/src/index.js"],
  "smithers-orchestrator/jsx-runtime": ["./packages/smithers/src/jsx-runtime.js"],
  "smithers-orchestrator/jsx-dev-runtime": ["./packages/smithers/src/jsx-runtime.js"],
  "smithers-orchestrator/tools": ["./packages/smithers/src/tools.js"],
  "smithers-orchestrator/*": [
    "./packages/smithers/src/*.js",
    "./packages/smithers/src/*/index.js"
  ],
  "smithers-orchestrator/scorers": ["./packages/scorers/src/index.js"],
  "@smithers-orchestrator/agents": ["./packages/agents/src/index.js"],
  "@smithers-orchestrator/gateway-client": ["./packages/gateway-client/src/index.ts"],
  "@smithers-orchestrator/gateway-react": ["./packages/gateway-react/src/index.ts"],
  "@smithers-orchestrator/pi-plugin": ["./packages/pi-plugin/src/index.ts"],
  "@smithers-orchestrator/server": ["./packages/server/src/index.js"]
  // The root tsconfig includes the same style of alias for each workspace package.
}
```

The root package is a private `smithers-monorepo`; `smithers-orchestrator` resolves to `packages/smithers`.

**End users do not need path aliases**, only framework developers do. Installing `smithers-orchestrator` as a dependency lets Node/Bun module resolution handle import paths automatically.

### Local Type Root Shims

```json
"typeRoots": ["./packages/smithers/src/types", "./node_modules/@types"]
```

The `./packages/smithers/src/types` directory contains ambient type declarations that fill gaps in third-party packages. One shim ships today:

- `react-dom-server.d.ts` -- Declares the `react-dom/server` module so TypeScript doesn't error when server-side rendering types are referenced.

End users should add `@types/react-dom` to `devDependencies` instead of relying on this shim.

## Bun Configuration

### Runtime Preload

```toml
# bunfig.toml
preload = ["./preload.js"]
```

The preload script registers the MDX esbuild plugin with Bun's bundler so `.mdx` files can be imported as JSX components at runtime. See MDX Prompts for details.

### Test Configuration

```toml
[test]
root = "."
preload = ["./preload.js"]
```

| Key | Value | Purpose |
|---|---|---|
| `root` | `.` | Bun discovers test files from the repository root |
| `preload` | `["./preload.js"]` | Registers the MDX plugin for test files so `.mdx` imports work in tests |

The test preload is separate from the runtime preload. Both point to the same file, but Bun's `[test]` section only applies when running `bun test`. Without it, tests that import `.mdx` files fail with a module resolution error.

## npm Scripts

Defined in the root `package.json` for development:

| Script | Command | Purpose |
|---|---|---|
| `typecheck` | `tsc --noEmit` | Type-check the `src/` and `tests/` trees against `tsconfig.json` |
| `typecheck:examples` | `tsc -p examples/tsconfig.json --noEmit` | Type-check example files against a separate config that maps `smithers-orchestrator` to `examples-entry.js` |
| `typecheck:evals` | `tsc -p evals/tsconfig.json --noEmit` | Type-check the agent-fluency eval suite under `evals/` against its own config |
| `lint` | `node scripts/lint.mjs` | Lint package source and tests with oxlint (cross-platform glob expansion) |
| `lint:fix` | `node scripts/lint.mjs --fix --fix-suggestions` | Apply supported oxlint fixes |
| `cli` | `bun run apps/cli/src/index.js` | Run the local development CLI entrypoint |
| `generate:init-pack` | `bun scripts/generate-workflow-pack.ts` | Regenerate the local workflow pack used by `bunx smithers-orchestrator init` |
| `sota:gen` | `bun scripts/generate-sota.ts` | Render the SOTA model registry (`docs/data/sota-models.json`) into its docs page and CLI module |
| `sota:research` | `bun scripts/sota-research.ts` | Research whether new SOTA models shipped and rewrite the registry if so (used by the daily cron) |
| `test` | `node scripts/check-single-effect-version.mjs && node scripts/check-dependency-boundaries.mjs && node scripts/check-no-direct-db-access.mjs && node scripts/check-docs.mjs && node scripts/check-llms.mjs && node scripts/check-sota.mjs && node scripts/check-dts.mjs && node scripts/check-smithers-test-script.mjs && pnpm -r --no-bail test` | Run docs/config guards, dependency guards, direct DB access guard, llms guards, SOTA registry guard, per-file declaration freshness, and each package test script (no-bail so every failing package is reported) |
| `coverage` | `node scripts/coverage.mjs` | Generate a test coverage report across the workspace |
| `check:effect` | `node scripts/check-single-effect-version.mjs` | Verify the workspace resolves a single Effect version |
| `check:deps` | `node scripts/check-dependency-boundaries.mjs` | Verify package dependency boundaries |
| `check:db-access` | `node scripts/check-no-direct-db-access.mjs` | Verify direct run/store DB access remains confined to the tracked issue #470 baseline |
| `check:docs` | `node scripts/check-docs.mjs` | Verify documentation style and API drift guards |
| `check:llms` | `node scripts/check-llms.mjs` | Verify generated llms docs are current |
| `check:sota` | `node scripts/check-sota.mjs` | Verify the SOTA registry's generated docs page and CLI module match `docs/data/sota-models.json` |
| `check:dts` | `node scripts/check-dts.mjs` | Verify per-file `.d.ts` declarations are freshly regenerated from source |
| `fetch:jj` | `node scripts/fetch-jj-binaries.mjs` | Fetch vendored jj binaries for supported platforms |
| `docs:components` | `node scripts/generate-component-source.mjs` | Embed composite component source as tabs in their docs pages |
| `docs:llms` | `bun scripts/generate-llms.ts && bun scripts/optimize-llms-full.ts` | Regenerate `llms-core.txt` and `llms-full.txt` |
| `docs` | `cd docs && bunx mintlify dev` | Start the Mintlify docs dev server for local preview |
| `release:content` | `bunx smithers-orchestrator up .smithers/workflows/release-content.tsx` | Run the release-content workflow (changelog, tweet thread, blog) in dry-run mode |
| `release:content:approve` | `bunx smithers-orchestrator up .smithers/workflows/release-content.tsx --input '{"dryRun":false,"publish":false}'` | Generate release content for real but stop before publishing |
| `release:content:publish` | `bunx smithers-orchestrator up .smithers/workflows/release-content.tsx --input '{"dryRun":false,"publish":true}'` | Generate and publish the release content |
| `release:workflow` | `bunx smithers-orchestrator up .smithers/workflows/release.tsx` | Run the end-to-end release workflow |
| `release:workflow:gated` | `bunx smithers-orchestrator up .smithers/workflows/release.tsx --input '{"requireMarketingContent":true}'` | Run the release workflow but require marketing content before publishing |
| `marketing:thread` | `bunx smithers-orchestrator up .smithers/workflows/release-content.tsx --input '{"channels":{"changelog":false,"tweetThread":true,"blogPost":false},"skip":{"changelog":true,"blogPost":true,"publishX":true,"publishBlog":true,"publishChangelog":true}}'` | Generate just the tweet thread in dry-run mode, skipping changelog and blog |
| `marketing:thread:write` | `bunx smithers-orchestrator up .smithers/workflows/release-content.tsx --input '{"dryRun":false,"publish":true,"allowUnreviewedPublish":true,"channels":{"changelog":false,"tweetThread":true,"blogPost":false},"skip":{"changelog":true,"blogPost":true,"approval":true,"publishX":true,"publishBlog":true,"publishChangelog":true}}'` | Generate and publish just the tweet thread, skipping changelog, blog, and the approval gate |
| `version` | `node scripts/bump.mjs` | Bump package versions using the release helper |
| `release` | `node scripts/publish.mjs` | Publish packages using the release helper |
| `sandbox:up` | `bun scripts/sandbox.ts up` | Create a disposable GCP sandbox VM and open an SSH session into it |
| `sandbox:down` | `bun scripts/sandbox.ts down` | Delete the sandbox VM created by `sandbox:up` |

### For end-user projects

When scaffolding your own project (with `bunx smithers-orchestrator init` or manually), add a typecheck script:

```json
{
  "scripts": {
    "typecheck": "tsc --noEmit"
  }
}
```

See Production Project Structure for a complete user-project `package.json` example.

---

## VCS Helper Reference

> Public JJ and VCS helper APIs for repo detection, snapshot inspection, binary resolution, and workspace management.

Smithers exports a small VCS helper surface for applications that inspect or manage Jujutsu state directly.

Lightweight by design:

- every helper accepts an optional `cwd` to target a specific repository
- spawn failures are normalized instead of throwing, so they are safe to call even when `jj` is not installed
- workspace helpers try a few command shapes to tolerate JJ version drift
- JJ command helpers return `Effect` values, so direct callers must provide an Effect `CommandExecutor` layer

## Import

The root `smithers-orchestrator` facade exports the main JJ helpers:

```ts
import {
  runJj,
  getJjPointer,
  revertToJjPointer,
  isJjRepo,
  workspaceAdd,
  workspaceList,
  workspaceClose,
} from "smithers-orchestrator";
```

The lower-level VCS package also exports repository discovery, binary resolution, tooling preflight, and snapshot capture helpers:

```ts
import {
  captureWorkspaceSnapshot,
  findVcsRoot,
  resolveGitBinary,
  resolveJjBinary,
  vcsToolingStatus,
} from "@smithers-orchestrator/vcs";
```

Direct JJ helper calls need a platform layer:

```ts
import type * as CommandExecutor from "@effect/platform/CommandExecutor";
import * as BunContext from "@effect/platform-bun/BunContext";
import { Effect } from "effect";

type VcsEffect<A> = Effect.Effect<A, never, CommandExecutor.CommandExecutor>;

const runVcs = <A,>(effect: VcsEffect<A>) =>
  Effect.runPromise(effect.pipe(Effect.provide(BunContext.layer)));
```

Install `@effect/platform-bun` when using the Bun snippet, or use the equivalent `CommandExecutor` layer for another runtime.

## `runJj(args, opts?)`

Run an arbitrary `jj` command and capture its output.

```ts
const result = await runVcs(runJj(["status"], { cwd: "/path/to/repo" }));
```

```ts
type RunJjOptions = {
  cwd?: string;
};

type RunJjResult = {
  code: number;
  stdout: string;
  stderr: string;
};

function runJj(args: string[], opts?: RunJjOptions): VcsEffect<RunJjResult>;
```

Notes:

- returns `{ code: 127, stdout: "", stderr: "..." }` when `jj` cannot be started
- does not throw for ordinary process failures
- a raw escape hatch beyond the higher-level helpers below

## `getJjPointer(cwd?)`

Return the current workspace `change_id` for `@`, or `null` when JJ is unavailable or the current directory is not a JJ repo.

```ts
const pointer = await runVcs(getJjPointer("/path/to/repo"));
```

```ts
function getJjPointer(cwd?: string): VcsEffect<string | null>;
```

Smithers uses the same pointer model internally for revert support and cache invalidation.

## `revertToJjPointer(pointer, cwd?)`

Restore the working copy from a previously recorded JJ pointer. A pointer is a JJ `change_id` string, as returned by `getJjPointer`.

```ts
const result = await runVcs(revertToJjPointer("zqkopwvn", "/path/to/repo"));
```

```ts
type JjRevertResult = {
  success: boolean;
  error?: string;
};

function revertToJjPointer(pointer: string, cwd?: string): VcsEffect<JjRevertResult>;
```

This helper wraps `jj restore --from <pointer>`.

## `isJjRepo(cwd?)`

Detect whether a directory is a readable JJ repository.

```ts
const enabled = await runVcs(isJjRepo("/path/to/repo"));
```

```ts
function isJjRepo(cwd?: string): VcsEffect<boolean>;
```

Use this before showing JJ-specific UI or attempting a revert flow.

## `workspaceAdd(name, path, opts?)`

Create a JJ workspace with a friendly name at a target filesystem path.

```ts
const result = await runVcs(
  workspaceAdd("feature-auth", "/tmp/wt-feature-auth", {
    cwd: "/path/to/repo",
    atRev: "@",
  }),
);
```

```ts
type WorkspaceAddOptions = {
  cwd?: string;
  atRev?: string;
};

type WorkspaceResult = {
  success: boolean;
  error?: string;
};

function workspaceAdd(name: string, path: string, opts?: WorkspaceAddOptions): VcsEffect<WorkspaceResult>;
```

Behavior notes:

- removes an existing workspace with the same name before retrying
- removes the target directory if it exists and creates its parent directory if needed
- tries multiple `jj workspace add` syntaxes to work across JJ versions

## `workspaceList(cwd?)`

List known workspaces for the current JJ repo.

```ts
const workspaces = await runVcs(workspaceList("/path/to/repo"));
```

```ts
type WorkspaceInfo = {
  name: string;
  path: string | null;
  selected: boolean;
};

function workspaceList(cwd?: string): VcsEffect<WorkspaceInfo[]>;
```

Prefers template output when supported, falls back to parsing the human-readable `jj workspace list` output.

## `workspaceClose(name, opts?)`

Forget a JJ workspace by name.

```ts
const result = await runVcs(
  workspaceClose("feature-auth", {
    cwd: "/path/to/repo",
  }),
);
```

```ts
function workspaceClose(
  name: string,
  opts?: { cwd?: string },
): VcsEffect<WorkspaceResult>;
```

This wraps `jj workspace forget <name>`.

## `captureWorkspaceSnapshot(cwd?)`

Capture the current JJ working-copy state as a restorable handle. This helper is exported by `@smithers-orchestrator/vcs`, not by the root facade.

```ts
const snapshot = await runVcs(captureWorkspaceSnapshot("/path/to/repo"));
```

```ts
type WorkspaceSnapshot = {
  commitId: string;
  changeId: string;
  operationId: string;
};

function captureWorkspaceSnapshot(cwd?: string): VcsEffect<WorkspaceSnapshot | null>;
```

It returns `null` on failures and timeouts, including non-JJ directories. Smithers uses the `commitId` and `operationId` values for workspace durability checkpoints.

## `findVcsRoot(startDir)`

Walk upward from a directory and return the nearest `.jj` or `.git` root. JJ wins when both markers exist in the same directory.

```ts
const root = findVcsRoot(process.cwd());
```

```ts
type VcsRoot =
  | { type: "jj"; root: string }
  | { type: "git"; root: string };

function findVcsRoot(startDir: string): VcsRoot | null;
```

## `resolveGitBinary()` and `resolveJjBinary()`

Resolve the executable Smithers will spawn for VCS commands.

```ts
const git = resolveGitBinary();
const jj = resolveJjBinary();
```

```ts
type ResolvedBinary = {
  path: string;
  source: "env" | "bundled" | "path";
};

function resolveGitBinary(): ResolvedBinary;
function resolveJjBinary(): ResolvedBinary;
```

`resolveGitBinary()` checks `SMITHERS_GIT_PATH`, then falls back to `git` on `PATH`. `resolveJjBinary()` checks `SMITHERS_JJ_PATH`, then a bundled `@smithers-orchestrator/jj-<platform>` package, then `jj` on `PATH`.

## `vcsToolingStatus()`

Probe whether the resolved VCS binaries are usable on the current host.

```ts
const status = vcsToolingStatus();
```

```ts
type VcsToolingStatus = {
  jj: ResolvedBinary | null;
  git: ResolvedBinary | null;
  ok: boolean;
};

function vcsToolingStatus(): VcsToolingStatus;
```

This is synchronous and best-effort. It runs version probes with a short timeout and powers the CLI's VCS preflight checks.

## When To Use These Helpers

Use these helpers when your application needs to:

- show whether JJ-backed revert is available
- record or inspect a pointer outside the Smithers engine
- manage JJ workspaces directly from an app or integration layer
- check whether JJ or Git tooling is available before starting worktree logic

For workflow-level revert behavior, prefer the runtime and CLI docs:

- VCS Integration
- CLI Reference
- Revert

---

## Gateway UI Components

> Prebuilt React components (RunList, RunTree, RunEventLog, ApprovalPanel, LaunchButton, and more) for assembling a custom workflow UI in minutes, built on the gateway-react hooks.

`@smithers-orchestrator/gateway-ui` is a set of drop-in React components for
building a custom workflow UI. They are built on the
`gateway-react` hooks, so each one connects to the
Gateway by itself: give it a `runId` or a filter and it renders live data and
wires the actions. Reach for these instead of hand-wiring hooks when you want a
run dashboard fast.

Import them from the `smithers-orchestrator/gateway-ui` subpath, alongside
`createGatewayReactRoot` from `gateway-react`. Every component is styled with
inline styles (no CSS import), so it bundles cleanly through the Gateway's
bundler, and every component accepts `className` and `style` to override.

The inline styles are theme-following: every color is a
`var(--token, #lightFallback)` expression over the
workflow UI style-guide tokens, which the
Gateway's HTML shell injects into every `/workflows/<key>` page. Components
therefore render light or dark with the rest of the page, following the OS
`prefers-color-scheme` or an explicit `data-theme="dark|light"` on `<html>`
(which the shell sets from a `?theme=` query param). Outside a themed page the
fallbacks reproduce the light look. Tints derive via `color-mix` over the same
tokens, never hex-plus-alpha.

## Components

| Component | What it renders | Key props |
| --- | --- | --- |
| `RunList` | A live, selectable list of runs with a status pill per row. | `filter`, `activeRunId`, `onSelect`, `pollMs` |
| `RunTree` | The node tree for a run (initial snapshot plus live updates). | `runId`, `onSelectNode`, `activeNodeId` |
| `RunEventLog` | A scrolling, auto-following event log for a run. | `runId`, `maxEvents`, `follow` |
| `NodeOutputView` | The output of a single node, fetched on demand. | `runId`, `nodeId`, `iteration` |
| `ApprovalPanel` | The pending approval queue with Approve and Deny buttons. | `filter`, `pollMs`, `onError` |
| `LaunchButton` | A button that launches a workflow run. | `workflow`, `input`, `onLaunched` |
| `WorkflowPicker` | A select of the workflows registered on the Gateway. | `value`, `onChange`, `hasUiOnly` |
| `ConnectionBadge` | A live Gateway connection indicator. | none |
| `StatusPill` | A colored status badge for any run or node status string. | `status`, `label` |

`StatusPill`, `statusColor`, and the `theme` style tokens are also exported for
building your own rows that match the components.

## A complete UI

This is a full `.smithers/ui/<workflow>.tsx` bundle: a run picker, the node tree,
a live event log, a node-output pane, and the approval queue, wired together with
local state. Save it next to a workflow, declare it from that workflow with
`<UI entry="../ui/<workflow>.tsx" />`, and open it with
`bunx smithers-orchestrator ui RUN_ID` (or `bunx smithers-orchestrator ui --workflow <workflow>`).
To open the full local control surface instead of a single workflow UI, see
`bunx smithers-orchestrator ui --app` below.

```tsx
/** @jsxImportSource react */
import { useState } from "react";
import { createGatewayReactRoot } from "smithers-orchestrator/gateway-react";
import {
  ApprovalPanel,
  ConnectionBadge,
  LaunchButton,
  NodeOutputView,
  RunEventLog,
  RunList,
  RunTree,
  WorkflowPicker,
} from "smithers-orchestrator/gateway-ui";

const WORKFLOW = "implement";

function App() {
  const [runId, setRunId] = useState<string>();
  const [nodeId, setNodeId] = useState<string>();

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, padding: 16 }}>
      <header style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <h1 style={{ margin: 0, fontSize: 18 }}>Implement</h1>
        <ConnectionBadge />
        <span style={{ flex: 1 }} />
        <LaunchButton workflow={WORKFLOW} onLaunched={setRunId} />
      </header>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }}>
        <RunList filter={{ workflow: WORKFLOW, limit: 20 }} activeRunId={runId} onSelect={setRunId} />
        <RunTree runId={runId} activeNodeId={nodeId} onSelectNode={(node) => setNodeId(node.id)} />
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <NodeOutputView runId={runId} nodeId={nodeId} />
          <RunEventLog runId={runId} style={{ height: 240 }} />
        </div>
      </div>

      <ApprovalPanel filter={{ workflow: WORKFLOW }} />
    </div>
  );
}

createGatewayReactRoot(<App />);
```

That is the whole UI. `RunList` polls for new runs, `RunTree` and `RunEventLog`
stream live as the run advances, `ApprovalPanel` lets you clear human gates, and
`LaunchButton` starts a fresh run. Mix these with raw
`gateway-react` hooks whenever you need something the
components do not cover.

## The full local Smithers UI

`bunx smithers-orchestrator ui --app` serves the full local control surface (`apps/smithers`)
instead of a single workflow's UI. It builds the app bundle on first use, serves
it same-origin with a local Gateway (reverse-proxying the Gateway's RPC,
WebSocket, and health paths), and opens it in your browser as a dashboard for
your runs, approvals, workspace files, and VCS state.

### Prerequisites

The app ships a local "concierge" chat that answers questions and backgrounds
workflows for you, so `--app` fails fast unless both of these hold:

- A chat credential. Set exactly one of `CEREBRAS_API_KEY` (recommended,
  fastest), `OPENAI_API_KEY`, `CODEX_ACCESS_TOKEN`, or `CODEX_REFRESH_TOKEN`.
  With none set, the command exits before it binds a port and prints which
  variable to set.
- `bun` on your `PATH`. The concierge runs on Bun, so the command checks for it
  up front and exits with an install hint if it is missing.

```bash
CEREBRAS_API_KEY=csk-... bunx smithers-orchestrator ui --app
```

### The concierge

The concierge is a small local chat backend the app talks to at `/api/chat`. Its
system prompt is seeded with the live catalog of workflows registered on your
Gateway, so it can background any of them for you, including `create-workflow`,
the meta-workflow that authors a brand-new workflow. It streams answers back
token by token, binds to loopback only, and needs no cloud account.

### Local workspace endpoints

Alongside the Gateway proxy, the `--app` server exposes a few loopback-only
endpoints the app uses to read and edit your working tree:

- `GET /api/files/tree`, `GET /api/files/read`, and `POST /api/files/write` for
  the file browser and editor.
- `/__smithers/vcs` for the local VCS (jj/git) status the app renders.
- `/__smithers/local-workspace` and `/__smithers/local-workspace/readiness` for
  workspace detection and readiness against the proxied Gateway.

These bind to `127.0.0.1` only and are scoped to the directory you launch from.

### Flags

| Flag | Default | What it does |
| --- | --- | --- |
| `--app` | `false` | Serve the full local UI instead of a single workflow run UI. |
| `--app-port <port>` | `7332` | Port to serve the full UI on. |
| `--rebuild` | `false` | Force a rebuild of the UI bundle before serving. |
| `--gateway <url>`, `-g` | `http://127.0.0.1:<port>` | Gateway base URL to proxy and read runs from. |
| `--port <port>` | `7331` | Gateway port when `--gateway` is not set. |
| `--no-open` | opens a browser | Print the URL instead of opening a browser. |
| `--no-autostart` | autostarts | Do not start a Gateway if none is reachable. |
| `--no-daemon` | daemon allowed | Force direct/embedded operation and never start a background Gateway daemon. |

Without `--app`, `bunx smithers-orchestrator ui [runId]` opens a single workflow run's custom UI
(the component gallery above) against the Gateway rather than the full surface.

## When to drop to hooks

The components own the common shapes. When you need a bespoke layout, an
embedded editor, or a custom card, read the same hooks the components use
(`useGatewayRuns`, `useGatewayRunTree`, `useGatewayRunEvents`,
`useGatewayApprovals`, `useGatewayActions`) directly. See
Custom Workflow UIs for the full hook walkthrough
and Workflow UI (React) for a hand-wired example.

---

## Custom Workflow UIs

> Build a first-class browser UI for your workflow with smithers ui, the Gateway, gateway-client, and gateway-react.

A custom workflow UI is a small browser bundle that *belongs to your workflow*
and talks to the Smithers Gateway through its domain API, RPC API, and live
event streams. Declare it in the workflow with `<UI entry="../ui/<workflow>.tsx"
/>`, keep the browser code in `.smithers/ui/<workflow>.tsx`, and the Gateway
builds and serves it at `/workflows/<key>`. `bunx smithers-orchestrator ui`
opens it for a run with a stable `?runId=` deep link.

This guide covers the whole shape: vanilla SDK boot, React hooks, the boot config the iframe receives, live event subscriptions (with automatic pushed updates and resilient reconnection), node-output and diff reads, approvals and signals, the crucial stale-data-free update model to avoid data bleeding across runs, DevTools observability streams, sample tests, and the same-origin proxy patterns you reach for when custom UIs run through smithers ui and the Gateway.

For the underlying RPC and event protocol, see Gateway. For the TanStack DB collection layer that backs the React hooks, see Sync. For two compact end-to-end examples, see Workflow UI (React) and Workflow UI (Vanilla). For the *visual* language every workflow UI shares (tokens, type, depth, motion) so they look like one product, see Workflow UI Design Principles.

<Note>API reference: Gateway React lists every hook and component, its options, and links to source and tests.</Note>

<Tip>Want a dashboard without wiring hooks by hand? `@smithers-orchestrator/gateway-ui` ships prebuilt components (`RunList`, `RunTree`, `RunEventLog`, `ApprovalPanel`, `LaunchButton`, and more) that each connect to the Gateway by themselves. Compose a full UI in a dozen lines, then drop to the hooks below only where you need something custom.</Tip>

Local launch is one command:

```sh
bunx smithers-orchestrator ui RUN_ID
```

If no Gateway is reachable on the local port, `bunx smithers-orchestrator ui` starts `bunx smithers-orchestrator gateway` for the workspace and waits for it before opening the browser. A hand-written `.smithers/gateway.ts` is not required for local UI launch as long as the workflow declares `<UI>`. Use `--no-autostart` to require an already-running Gateway, or `--gateway <url>` to point at a remote one.

<Note>Moving an older `.smithers/ui/<workflow>.tsx` auto-mount or `gateway.register(..., { ui })` setup? See Migrate to Workflow-Owned UIs.</Note>

## How a custom UI is wired

Three pieces collaborate:

1. **The workflow declares its UI.** Add `<UI entry="../ui/vcs.tsx" title="VCS" />` inside `.smithers/workflows/vcs.tsx`. When the launcher calls `gateway.register("vcs", vcs, { entryFile })`, the Gateway discovers that declaration, bundles the entry file into a single browser script, and serves it at `/workflows/vcs`. It also exposes a `GET /workflows/<key>/boot` config that the rendered page hydrates from.

2. **The Gateway serves an HTML shell.** Hitting `/workflows/vcs` returns a tiny HTML document with a `<div id="root">`, the bundled script, and a global `__SMITHERS_GATEWAY_UI__` object (the `GatewayUiBootConfig`) set before your script runs. The shell uses `?runId=<id>` from the query string to scope a session to one run; if it is omitted, your UI is responsible for showing a picker or empty state. The shell is also **theme-aware**: it inlines the workflow UI style-guide tokens (light values, dark overrides, and `color-scheme`) so the document follows the OS `prefers-color-scheme` before your bundle loads, and it honors an explicit `?theme=dark` / `?theme=light` query param by stamping `data-theme` on `<html>` pre-paint. That query param is the channel an embedding host (for example an app rendering the page in an iframe) uses to push its own light/dark toggle into the UI.

3. **smithers ui opens the shell same-origin.** `bunx smithers-orchestrator ui RUN_ID` resolves the run's workflow, ensures a Gateway is reachable, and opens `/workflows/<key>?runId=<id>` in the browser. Because the page is served by the Gateway origin, its RPC calls and WebSocket streams reach the real Gateway without CORS or token shuttling.

## Two SDKs: pick by appetite

| | `smithers-orchestrator/gateway-client` | `smithers-orchestrator/gateway-react` |
|---|---|---|
| **Footprint** | One zero-dep class, no framework | React 19 + ReactDOM + hooks |
| **Boot** | `new SmithersGatewayClient()` | `createGatewayReactRoot(<App />)` |
| **Live events** | `streamRunEventsResilient` async generator | `useGatewayRunEvents(runId)` over TanStack DB |
| **Reads** | `client.getNodeOutput({...})` and REST domain calls | `useGateway*` hooks or `useLiveQuery` over collections |
| **Writes** | `client.submitApproval(...)` and REST domain writes | `useGatewayActions()` or collection mutation handlers |
| **Use when** | You want one tiny bundle, or you already own your render layer (Solid, Lit, vanilla DOM) | You are writing a React UI and want the stale-data-free model baked in |

Both SDKs talk to the same Gateway. The vanilla SDK is published as `@smithers-orchestrator/gateway-client`; the React layer is `@smithers-orchestrator/gateway-react` and adds providers plus hooks over the TanStack DB collections.

## The shared component library

Before styling anything by hand, reach for `smithers-orchestrator/ui` (the published `@smithers-orchestrator/ui` package). Prefer the `smithers-orchestrator/ui` subpath, which resolves through the `smithers-orchestrator` install you already have; importing the scoped `@smithers-orchestrator/ui` name directly requires declaring it as a direct dependency under pnpm-style isolated layouts. It ships shadcn-anatomy components (variant props, `asChild` slots, Radix behavior for dialogs, tabs, tooltips, and selects) that are styled entirely through the workflow UI theme tokens, so every component is correct in light and dark automatically: the OS `prefers-color-scheme` is respected, and an explicit `?theme=dark|light` on the host page always wins.

```tsx .smithers/ui/triage.tsx
/** @jsxImportSource react */
import { useState } from "react";
import { createGatewayReactRoot, useGatewayActions, useGatewayRuns } from "smithers-orchestrator/gateway-react";
import { WorkflowUiShell } from "smithers-orchestrator/gateway-ui";
import {
  Button,
  Card,
  CardHeader,
  CardTitle,
  EmptyState,
  Input,
  RowButton,
  SmithersUiStyles,
  StatusPill,
} from "smithers-orchestrator/ui";

function App() {
  const [prompt, setPrompt] = useState("");
  const runs = useGatewayRuns({ filter: { limit: 20 } });
  const actions = useGatewayActions();
  return (
    <WorkflowUiShell title="Triage">
      <SmithersUiStyles />
      <Card>
        <CardHeader>
          <CardTitle>Launch</CardTitle>
        </CardHeader>
        <div style={{ display: "flex", gap: 8 }}>
          <Input style={{ flex: 1 }} value={prompt} onChange={(e) => setPrompt(e.currentTarget.value)} placeholder="Optional prompt..." />
          <Button onClick={() => void actions.launchRun({ workflow: "triage", input: { prompt } })}>Start</Button>
        </div>
      </Card>
      {(runs.data ?? []).length === 0 ? (
        <EmptyState title="No runs yet" description="Launch one to see it here." />
      ) : (
        (runs.data ?? []).map((r) => (
          <RowButton key={r.runId}>
            <span className="mono">{r.runId.slice(0, 8)}</span>
            <StatusPill status={r.status} />
          </RowButton>
        ))
      )}
    </WorkflowUiShell>
  );
}

createGatewayReactRoot(<App />);
```

The important mechanics:

- **Styles travel as a JS string, never a `.css` import.** The Gateway bundler keeps only the JS output, so `import "./x.css"` is silently dropped. Render `<SmithersUiStyles/>` once at the root; every component also self-injects the sheet in the browser as a fallback, so a missing style tag degrades gracefully instead of unstyled.
- **All classes are namespaced `sui-*`**, so they can never collide with the styleguide page vocabulary (`.button`, `.badge`, `.card`) or your own CSS. Your escape hatch for custom rules is `<SmithersUiStyles extra=".mine { ... }" />` or a plain `<style>` string of your own.
- **Standalone hosts** (pages the Gateway shell does not serve) pass `withTheme` to prepend the token block: `<SmithersUiStyles withTheme />`.
- **The status vocabulary is shared.** `normalizeStatus`, `statusClass`, `formatStatus`, and `isTerminalRunStatus` are exported from `smithers-orchestrator/ui` (and re-exported by `gateway-ui`), so stop re-implementing them per UI.

The component set: `Button` (the default variant is the house tinted-brand primary; `solid` is the filled look), `Card` family, `Badge` and `StatusPill`, `Tabs` (with a `count` slot per trigger), `Dialog` (backdrop, Esc, focus trap for free), `Tooltip`, `Select`, `Input`/`Textarea`/`Label`/`Field`, `Alert` (the honest-degradation surface), `Table`, `Progress`, `Skeleton`, `Spinner`, `Separator`, `EmptyState`, `SectionHeader`/`Eyebrow`, `RowButton`, and `KpiStat`.

For run-aware building blocks that already talk to the Gateway (`RunList`, `RunTree`, `RunEventLog`, `ApprovalPanel`, `SimpleWorkflowDashboard`), use `@smithers-orchestrator/gateway-ui`; it is built on the same tokens and re-exports the status helpers.

## The boot config

Every page the Gateway serves at `/workflows/<key>` (and `/inspector/RUN_ID` for the built-in inspector) sets a global before your bundle parses:

```ts
declare global {
  var __SMITHERS_GATEWAY_UI__: GatewayUiBootConfig | undefined;
}

type GatewayUiBootConfig = {
  apiVersion: "v1";
  kind: "gateway" | "workflow";
  workflowKey: string | null;
  mountPath: string;     // where the Gateway mounted you, e.g. "/workflows/vcs"
  rpcPath: string;       // "/v1/rpc"
  wsPath: string;        // "/": the WebSocket path under the Gateway origin
  assetBasePath: string; // where to resolve relative bundle assets
  props: Record<string, unknown>; // free-form, set by `<UI props={...} />`
};
```

You do not normally touch this. `new SmithersGatewayClient()` reads it automatically. Its HTTP RPC wrapper calls `/v1/rpc/<method>` under `baseUrl`, while WebSocket streams use the boot `wsPath`. Reach for the boot config directly when you need a UI-specific prop (`__SMITHERS_GATEWAY_UI__?.props.brand`), a direct `fetch` target (`rpcPath`), a CDN asset (`assetBasePath`), or to react to whether the Gateway mounted you for one workflow or for the global inspector (`kind === "workflow"`).

The `?runId=` query parameter is **not** in the boot config; it is in `location.search`, because a single UI bundle may be invoked across many runs. Read it once at startup and pass it through your store:

```ts
const runId = new URLSearchParams(location.search).get("runId") ?? undefined;
```

## React: `createGatewayReactRoot` and hooks

`createGatewayReactRoot` is the one-line bootstrap: it reads the boot config, constructs a `SmithersGatewayClient`, mounts the `SmithersGatewayProvider`, and renders your tree. The provider also mounts `SmithersCollectionsProvider`, which creates the TanStack DB collection registry for the current `WorkspaceMode`. Local mode reads through `/v1/api/*` and refreshes from `/v1/api/stream`; multiplayer mode reads through Electric shapes served by `@smithers-orchestrator/electric-proxy`.

```tsx
/** @jsxImportSource react */
import {
  createGatewayReactRoot,
  useGatewayRun,
  useGatewayRunEvents,
  useGatewayNodeOutput,
  useGatewayActions,
} from "smithers-orchestrator/gateway-react";

const runId = new URLSearchParams(location.search).get("runId") ?? undefined;

function App() {
  const run = useGatewayRun(runId);                   // run record + optional runState, refetches when runId changes
  const events = useGatewayRunEvents(runId);          // live stream, resilient reconnect, gap resync
  const output = useGatewayNodeOutput({               // last task output, refetches when the input key changes
    runId,
    nodeId: "ship",
  });
  const { submitApproval, cancelRun } = useGatewayActions();

  return (
    <main>
      <h1>{run.data?.workflowKey} · {run.data?.status}</h1>
      <button onClick={() => cancelRun({ runId: runId! })}>Cancel</button>
      <pre>{JSON.stringify(output.data?.row, null, 2)}</pre>
      <ul>{events.events.map((f) => <li key={f.seq}>{f.event}</li>)}</ul>
    </main>
  );
}

createGatewayReactRoot(<App />);
```

The React hooks all live in `packages/gateway-react/src` and follow one rule: **they never surface a previous run's data after the inputs change.** Collection hooks key their TanStack DB query to the current input, and RPC escape hatches still clear `data` and `error` synchronously before issuing a fresh fetch. A late response from the previous `runId` cannot repopulate the cleared state, so the UI never blinks the wrong run between transitions. See Stale-data-free update model for why this matters at the iframe boundary.

| Hook | Returns | Notes |
|---|---|---|
| `useSmithersGateway()` | `SmithersGatewayClient` | Bare client; fall back to it for one-off RPC. |
| `useGatewayRun(runId)` | `{ data: Record<string, unknown>, loading, error, refetch }` | Reads the `getRun` payload: a run record with `summary` and optional `runState: RunStateView`. Refetches when `runId` changes. Disabled when `runId` is `undefined`. |
| `useGatewayRuns({ filter? })` | `GatewayAsyncState<Record<string, unknown>[]>` | List recent runs; `filter` accepts `status`, `limit`. |
| `useGatewayWorkflows()` | `GatewayAsyncState<ListWorkflowsResponse>` | Powers picker UIs. |
| `useGatewayRunEvents(runId, opts?)` | `{ events, lastHeartbeat, streaming, error }` | Live WebSocket subscription. Drops the oldest events past `maxEvents` (default 1000). |
| `useGatewayNodeOutput({ runId, nodeId, iteration? })` | `GatewayAsyncState<Record<string, unknown>>` | Reads a finished task's row + schema. |
| `useGatewayApprovals({ filter? })` | `GatewayAsyncState<ListApprovalsResponse>` | All gates waiting; `filter` accepts `runId`, `workflow`, `limit`. |
| `useGatewayActions()` | Bound `submitApproval`, `submitSignal`, `cancelRun`, `resumeRun`, `rewindRun`, `cronCreate`, … | Memoized so dependent effects do not retrigger. |
| `useGatewayRpc(method, params, opts?)` | `GatewayAsyncState<Payload>` | Escape hatch for any v1 RPC. |
| `useGatewayExtensionResource(namespace, key, params?, opts?)` | `GatewayAsyncState<T>` | Declarative extension read/action call over `SmithersGatewayClient.extensionRpc` with the same stale-response fence as `useGatewayRpc`. |
| `useGatewayExtensionAction(namespace, key)` | `{ call, pending, error, data }` | Imperative extension action helper backed by `extensionRpc`; generation-fenced so rapid calls cannot leave stale state. |
| `useGatewayExtensionStream(namespace, key, params?, opts?)` | `{ frames, latest, error, streaming }` | Extension stream subscription with bounded frames, stale-frame fencing, and backoff reconnect. |
| `useSmithersCollections()` | `{ client, collections, queryClient }` | Direct access to the TanStack DB collection registry for custom `useLiveQuery` reads. |
| `SmithersCollectionsProvider` | React provider | Advanced provider for supplying a custom `WorkspaceMode`, `SmithersDataClient`, or `QueryClient`. |

## Vanilla SDK

The same primitives in zero-dep JS:

```ts
import { SmithersGatewayClient } from "smithers-orchestrator/gateway-client";

const runId = new URLSearchParams(location.search).get("runId");
const client = new SmithersGatewayClient();

const run = await client.getRun({ runId });
const out = await client.getNodeOutput({ runId, nodeId: "ship" });

// Live subscription with automatic reconnect + gap resync.
const abort = new AbortController();
(async () => {
  for await (const frame of client.streamRunEventsResilient({ runId }, { signal: abort.signal })) {
    if (frame.event === "run.completed") render(await client.getRun({ runId }));
  }
})();

// Approvals / signals / cancellation are one-shot RPC.
await client.submitApproval({ runId, nodeId: "review", decision: { approved: true } });
await client.cancelRun({ runId });
```

`streamRunEventsResilient` is the option you almost always want over `streamRunEvents`: it reconnects with backoff + jitter on dropped sockets, resumes from the last per-run `seq`, and refuses to reset the backoff counter until a connection has proved itself with a live (non-replay) frame or by surviving a settle window. See the JSDoc on [`SmithersGatewayClient.streamRunEventsResilient`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/SmithersGatewayClient.ts) for the exact semantics.

## Auth

A custom UI almost never holds a token. The Gateway accepts auth via three mechanisms (`Authorization: Bearer <token>`, an `x-smithers-key` header, or a `trusted-proxy` mode that reads identity headers off an upstream proxy), and the right answer for an embedded UI is **trusted-proxy via same-origin**.

In practice this means one of:

- **Local bunx smithers-orchestrator ui.** The CLI opens the Gateway-served `/workflows/<key>?runId=<id>` page directly. The browser sees one origin and the Gateway can use the local token or key configured for that process. See Local dev setup.
- **Your own host.** Put the Gateway behind a reverse proxy that serves `/v1/rpc`, `/health`, and `/workflows/*` on the same origin as the page embedding the UI. A trusted proxy can terminate the user's session, strip browser-supplied identity headers, and forward only the identity headers it validated. See Trusted same-origin proxy.
- **Bring-your-own token.** Pass `new SmithersGatewayClient({ token, baseUrl })` if you really do hold a token at the UI layer. Useful for tooling and one-off bots; avoid it in user-facing surfaces.

The hooks read no auth state of their own. They call `client.rpc(...)`, and the client carries whatever `token` / `headers` you set when you constructed it (or that the trusted-proxy stripped and rewrote on the way in).

## Live subscriptions

`useGatewayRunEvents(runId)` opens one `streamRunEventsResilient` socket per `runId` and surfaces every non-heartbeat frame in the `events` array. A heartbeat updates `lastHeartbeat` instead. Heartbeats are how you detect a still-alive but quiet run, so they belong on their own pin to avoid bloating the events array. When `runId` changes the prior connection is aborted and the buffer resets, so the UI never shows the wrong run's events.

Each entry in `events` is a `GatewayEventFrame`: **`{ event, payload, seq }`**. The
event NAME is `frame.event` (`"NodeStarted"`, `"NodeFinished"`, `"NodeFailed"`,
`"RunStatusChanged"`, …) and everything else is under `frame.payload`. To build a
live per-task view, including surfacing a task that FAILED, key off
`frame.payload.nodeId` and read `frame.payload.error` on `NodeFailed`:

```tsx
const { events } = useGatewayRunEvents(runId, { afterSeq: 0 });
const byNode = new Map<string, { state: string; error?: string }>();
for (const frame of events) {
  const nodeId = (frame.payload as { nodeId?: string })?.nodeId;
  if (!nodeId) continue;
  if (frame.event === "NodeFailed") {
    const err = (frame.payload as { error?: { message?: string } }).error;
    byNode.set(nodeId, { state: "failed", error: err?.message });
  } else if (frame.event === "NodeFinished") byNode.set(nodeId, { state: "done" });
  else if (frame.event === "NodeStarted") byNode.set(nodeId, { state: "running" });
}
// render every node in byNode, showing its state + error; a failed task must be visible, not swallowed.
```

The vanilla equivalent (`for await (const frame of client.streamRunEventsResilient(...))`) gives you the same loop without React state. Either way the Gateway picks up where the last `seq` left off after a reconnect, replaying anything you missed as a `run.gap_resync` frame before resuming live `run.event`s.

When the run finishes, the server emits one `run.completed` frame and closes the stream cleanly; both layers stop reconnecting on that signal.

## Node output and diff reads

Every finished task has a structured output row (Zod-validated, JSON-serializable) and an optional diff payload (the snapshot of files the task changed).

```tsx
const ship = useGatewayNodeOutput({ runId, nodeId: "ship" });
// ship.data: { status: "produced", row: { ok: true, sha: "…" }, schema: {...} }

const diff = useGatewayRpc("getNodeDiff", { runId, nodeId: "ship" });
// diff.data: { status, files: [{ path, status, hunks?: [...] }], … }
```

A row arrives either inline (`status: "produced"`) or as a typed `pending` / `failed` state with an optional `partial` payload on failure. Node-output hooks default to a row-shaped value that you should destructure as either the row directly *or* `{ row, schema, status }`; `.smithers/ui/vcs.tsx` shows the canonical normalizer (`rowOf(value)`).

## DevTools observability streams

Beyond standard node output and run events, `streamDevTools` provides the live DevTools tree: an initial snapshot plus `devtools.event` delta frames such as `replaceRoot`, `addNode`, `removeNode`, `updateProps`, and `updateTask`. Use it for inspectors that need node props, task metadata, and rebaselining after rewind. It is a raw WebSocket iterator on `SmithersGatewayClient`; if you need reconnect behavior, re-subscribe with the last `afterSeq`.

## Approvals, signals, and lifecycle actions

`useGatewayActions()` returns a stable object of bound mutators. Use it for human-in-the-loop gates and for any lifecycle write:

```tsx
const { submitApproval, submitSignal, cancelRun } = useGatewayActions();

<button onClick={() => submitApproval({ runId, nodeId: "review", decision: { approved: true } })}>
  Approve
</button>

<button onClick={() => submitSignal({ runId, correlationKey: "utterance", payload: { text } })}>
  Send
</button>

<button onClick={() => cancelRun({ runId })}>Cancel</button>
```

The `correlationKey` passed to `submitSignal` must match the workflow wait's `correlationId`, for example `<WaitForEvent event="utterance" correlationId="utterance">`. The Gateway enforces approval scopes (`allowedScopes`, `allowedUsers`) per the workflow's `<Approval>` declaration, so a UI that surfaces "Approve" for an unauthorized user will still fail at the RPC boundary. Render the gate optimistically and let the error surface through `useGatewayActions`'s return value.

`useGatewayApprovals({ filter: { runId } })` lists every pending gate for the run;
pair it with `useGatewayActions().submitApproval` to drive a "pending approvals"
surface. Its **`data` IS the array** of gates (no `data.approvals` wrapper); each
gate is `{ runId, nodeId, iteration, requestTitle?, requestSummary?, workflowKey? }`
the title is `requestTitle`, not `title`. Feed `nodeId` + `iteration` straight
into `submitApproval`, and remember `decision` is required:

```tsx
const approvals = useGatewayApprovals({ filter: { runId } });
const { submitApproval } = useGatewayActions();
return (approvals.data ?? []).map((gate) => (
  <div key={gate.nodeId}>
    <span>{gate.requestTitle ?? gate.nodeId}</span>
    <button onClick={() => submitApproval({ runId, nodeId: gate.nodeId, iteration: gate.iteration, decision: { approved: true } })}>
      Approve
    </button>
  </div>
));
```

The reference UIs in `.smithers/ui/grill-me.tsx` and `.smithers/ui/ultragrill.tsx`
show this pattern at scale.

## Stale-data-free update model

The fundamental rule across both SDKs is: **a hook or read whose inputs have changed must never surface the previous inputs' result.** Without this, a UI that switches from `runId=A` to `runId=B` momentarily shows A's data before B's fetch lands. At the iframe boundary that looks like the embed *belongs* to the wrong run, which is the most confusing failure mode possible.

`useGatewayRpc` enforces this by:

1. **Clearing on input change.** When `runId`, `nodeId`, or any custom dep changes, the effect synchronously clears `data` and `error`, then fires a fresh fetch. Consumers see an empty state while the new fetch is in flight.
2. **Generation-tagged requests.** Each `refetch` reads a per-hook generation counter; only the latest generation can repopulate state. A late response from a previous inputs version is dropped on arrival.
3. **Disabling clears too.** `enabled: false` (e.g. when `runId` becomes `undefined`) clears state to empty rather than freezing the last known value.

`useGatewayRunEvents` does the same for the WebSocket: it aborts the prior stream and resets `events`/`lastHeartbeat` when `runId` changes, so no frame from the previous run can leak across the boundary.

If you build your own read on top of `useGatewayRpc` (via the `deps` option) or hand-roll one with the vanilla client, mirror the same pattern: clear the state when inputs change, and tag in-flight work so late responses are dropped.

## Same-origin proxy patterns

Custom UIs may be embedded in an iframe, or opened directly by bunx smithers-orchestrator ui. The robust path is to serve the UI shell and Gateway RPC from the same origin, so the page's `fetch("/v1/rpc/...")` and `new WebSocket("wss://<host>/")` both hit the Gateway without CORS or token shuttling.

### Local dev setup

For local work, use `bunx smithers-orchestrator ui` first. It starts or reuses a Gateway, resolves the run's workflow, and opens the Gateway-hosted `/workflows/<key>?runId=<id>` page:

```bash
bunx smithers-orchestrator up WORKFLOW -d
bunx smithers-orchestrator ui RUN_ID
```

If you want the Gateway to stay alive between browser launches, run it yourself and point the UI command at it:

```bash
bunx smithers-orchestrator gateway --port 7331
bunx smithers-orchestrator ui RUN_ID --gateway http://127.0.0.1:7331
```

The browser sees a single origin. The page at `/workflows/<key>` is served by the Gateway, so `new SmithersGatewayClient()` calls `fetch("/v1/rpc/getRun", ...)` on the same origin and streams use the boot `wsPath`. No CORS, no token shuttling.

### Trusted same-origin proxy

If you embed a custom workflow UI in your own host, put the Gateway behind a reverse proxy that forwards the Gateway route families on the host's origin:

```
/api/auth/*    →  GitHub (user session terminator)
/v1/rpc/*      →  Smithers Gateway   (RPC, trusted-proxy headers or service token attached)
/workflows/*   →  Smithers Gateway   (HTML shell + bundle)
/health        →  Smithers Gateway   (liveness)
```

The proxy usually has two Gateway-auth branches:

1. **Service-token branch.** If `GATEWAY_AUTH_TOKEN` is set, the proxy strips browser-supplied Gateway credentials and trusted-proxy headers, adds `Authorization: Bearer <service-token>`, and forwards the request without minting user identity headers.
2. **Trusted-proxy branch.** If no service token is configured, the proxy validates the user's session, strips client-supplied trusted-proxy headers (`x-user-id`, `x-user-scopes`, `x-user-role`, `x-smithers-token-id`), and re-injects them from the validated session plus a small allowlist of scopes (`run:read`, `run:write`, `approval:submit`, `signal:submit`, `cron:read`, `cron:write`, `observability:read`).

The Gateway auth mode determines which branch preserves per-user identity. In `mode: "trusted-proxy"`, the Gateway reads role, scopes, user id, and token id from the trusted headers the proxy set. In `mode: "token"` or `mode: "jwt"`, the Gateway reads the bearer credential and ignores trusted-proxy identity headers; use the service-token branch only when service identity is acceptable. The browser never sees a Gateway credential; the iframe's RPC calls and WebSocket upgrades flow through the proxy.

The same shape works behind any reverse proxy (Cloudflare, Cloudflare Access, Caddy, nginx, an internal API gateway): terminate session, strip identity headers off the request, set them from the validated session, forward to the Gateway. Trusted-proxy mode is **only safe behind something you control** that strips and rewrites identity headers; the Gateway docs call this out explicitly.

## Local dev quick reference

```bash
# Just open the UI. If no Gateway is reachable on the local port, `ui` starts
# one for you and serves workflow-owned <UI> declarations.
bunx smithers-orchestrator ui                          # picks the most recent run
bunx smithers-orchestrator ui RUN_ID                   # specific run

# Or run the Gateway yourself (to keep it alive / choose the port), then open:
bunx smithers-orchestrator gateway --port 7331         # serves every workspace workflow + declared <UI> entries
bunx smithers-orchestrator ui RUN_ID --gateway http://127.0.0.1:7331   # point ui at it (--no-autostart never spawns one)

```

The `bunx smithers-orchestrator ui` command resolves the most recent run when invoked without an id, prints the URL it opened, and exits. When no Gateway answers on the local port it **auto-starts one** and discovers the run's workflow-owned `<UI>` declaration; pass `--no-autostart` to disable that, or `--gateway <url>` to point at an existing one. Note: `up --serve` is a per-run HTTP server, **not** a full Gateway, so `ui` needs a Gateway.

## Sample tests

A custom UI has two layers worth testing:

1. **The bundle parses and boots without a network.** A Bun test that imports the module under a happy-dom registrator and asserts the React tree renders covers a regression-class of "the bundle broke because of a Vite/tsup mishap" failures cheaply. The reference is `packages/gateway-react/tests/gatewayReactBehavior.test.ts`; it constructs spy clients, drives every hook, and asserts they never surface a previous input's result after a re-render.

2. **The Gateway serves it and the run flows through.** A real-backend Playwright test that boots a Gateway, registers a workflow whose JSX graph declares `<UI entry="../ui/<key>.tsx" />`, executes a run to completion, and drives a real browser that:
   - deep-links to `/workflows/<key>?runId=<id>`,
   - asserts the iframe rendered the custom UI,
   - asserts `data-testid="demo-run-id"` matches `?runId=` (proof the boot path works),
   - flips to the native inspector and back.

The server package's Gateway UI tests are the closest in-repo reference for the serving contract. Two useful fixture styles are:

- A dependency-free vanilla bundle that reads `?runId=` from `location.search` and renders a heading. Use it to assert the bundle path without coupling to a UI library.
- The same demo on `gateway-react`. Drive `createGatewayReactRoot`, `useGatewayRun`, and `useGatewayActions` against the real fixture Gateway to prove the React hooks survive an iframe boundary, a stale-data transition, and a button-driven action.

Register your fixture with `gateway.register("<key>", workflow, { entryFile: "/path/to/.smithers/workflows/<key>.tsx" })` and put `<UI entry="../ui/<key>.tsx" />` inside that workflow. Execute the run to completion *before* binding the port so Playwright never races the run's tree.

## Reference

- **Package:** `@smithers-orchestrator/gateway-react`: React hooks and `createGatewayReactRoot`.
- **Package:** `@smithers-orchestrator/gateway-client`: vanilla `SmithersGatewayClient` and types.
- **Protocol:** Gateway: full RPC method list, WebSocket frame shapes, error codes, scopes.
- **CLI:** `bunx smithers-orchestrator ui`: open a workflow's custom UI in your browser for a given run.
- **Examples:** Workflow UI (React), Workflow UI (Vanilla).
- **Reference bundles in this repo:** `.smithers/ui/vcs.tsx`, `.smithers/ui/grill-me.tsx`, `.smithers/ui/ultragrill.tsx`, `.smithers/ui/workflow-skill.tsx`.

---

## runWorkflow

> Programmatic entry point. Equivalent to `bunx smithers-orchestrator up`.

```ts
import { runWorkflow } from "smithers-orchestrator";
import { Effect } from "effect";

const result = await Effect.runPromise(runWorkflow(workflow, {
  input: { task: "fix bug" },
}));

result.runId;     // string
result.status;    // "finished" | "failed" | "cancelled" | "continued" | "waiting-approval" | "waiting-event" | "waiting-timer" | "waiting-quota" | "paused"
result.output;    // populated only if your schema has a key literally named `output`
result.error;     // serialized SmithersError on failure
```

Signature:

```ts
function runWorkflow<Schema>(
  workflow: SmithersWorkflow<Schema>,
  opts: RunOptions,                 // see Types
): Effect.Effect<RunResult, SmithersError>;
```

Both `RunOptions` and `RunResult` are defined in Types.

## Resume

Pass the original `runId` plus `resume: true`. State loads from the configured durable store, completed tasks are skipped, in-progress attempts older than 15 minutes are abandoned and retried.

```ts
await Effect.runPromise(runWorkflow(workflow, { input: {}, runId: "my-run-123", resume: true }));
```

The original input row is loaded from durable state; pass `{}` for `input`. The workflow file hash and VCS root must match the original run.

## Cancel via AbortSignal

```ts
const controller = new AbortController();
setTimeout(() => controller.abort(), 5 * 60 * 1000);

const result = await Effect.runPromise(runWorkflow(workflow, { input: {...}, signal: controller.signal }));
// result.status === "cancelled"
```

All in-flight attempts are marked cancelled and `NodeCancelled` events are emitted.

## Hijack handoff

If a CLI hijack happens mid-run (`bunx smithers-orchestrator hijack RUN_ID`), the run ends `"cancelled"` and the latest attempt metadata stores `hijackHandoff`. On `resume: true`, Smithers waits for a safe handoff point and continues with the persisted CLI session id (see CLI Agents) or the persisted message history (SDK agents).

## `result.output`

Populated only when the schema passed to `createSmithers()` has a key literally named `output`. Inside a workflow, consume other typed rows through `ctx.output`, `ctx.latest`, or `ctx.outputs`. Outside the workflow, expose the run through the workspace Gateway and fetch a node's typed output through `getNodeOutput`:

```ts
import { SmithersGatewayClient } from "smithers-orchestrator/gateway-client";

const gateway = new SmithersGatewayClient({
  baseUrl: process.env.SMITHERS_GATEWAY_URL,
  token: process.env.SMITHERS_API_KEY,
});

const page = await gateway.getNodeOutput({
  runId: result.runId,
  nodeId: "render-page",
});
```

Controllers and monitors must not open SQLite/PGlite/Postgres or query output and
`_smithers_*` tables directly. The Gateway preserves auth, ownership,
invalidation, event emission, and backend independence. Direct storage access is
reserved for runtime implementation, migration, backend tests, and maintainer
diagnostics.

## Notes

- On macOS, `runWorkflow` acquires a `caffeinate` lock to prevent idle sleep and releases it on completion. On other platforms this is a no-op.
- Set `SMITHERS_LOG_LEVEL=debug` to enable verbose engine logging.
- For lifecycle events, pass `onProgress` (see Events).

---

## renderFrame

> Render a workflow tree to a GraphSnapshot without executing.

Use `renderFrame` to preview the task graph (for CI validation, graph-inspection UIs, or dry-run checks) without executing or persisting anything. `runId` and `iteration` are arbitrary strings used only for snapshot identity.

```ts
import { renderFrame } from "smithers-orchestrator";
import { SmithersCtx } from "@smithers-orchestrator/driver";
import { Effect } from "effect";

const ctx = new SmithersCtx({ runId: "preview", iteration: 0, input: { task: "preview" }, outputs: {} });
const snap = await Effect.runPromise(renderFrame(workflow, ctx));

snap.frameNo;       // 0
snap.tasks;         // TaskDescriptor[]
snap.xml;           // XmlNode | null (see Types)
```

`TaskDescriptor` and `GraphSnapshot` are defined in Types. Same shape the runtime extracts on every render frame; `renderFrame` doesn't execute or persist.

`outputs` lets you simulate completed upstream tasks:

```ts
const ctx = new SmithersCtx({
  runId: "sim", iteration: 0, input: { x: 1 },
  outputs: {
    analyze: [{ runId: "sim", nodeId: "analyze", iteration: 0, summary: "..." }],
  },
});
const snap = await Effect.runPromise(renderFrame(workflow, ctx));
```

CLI equivalent:

```bash
bunx smithers-orchestrator graph workflow.tsx --input '{"task":"preview"}'
```

---

## Revert to Attempt

> Restore the workspace to a previous task attempt's filesystem state via JJ.

Each successful task attempt captures the current [JJ](https://jj-vcs.github.io/jj/) change ID into `_smithers_attempts.jj_pointer`. `revert` restores the workspace to that state and discards any graph snapshots recorded after the attempt began, so the run's timeline rolls back to the point-in-time of that attempt.

<Note>API reference: Time Travel lists every revert and time-travel export, its options, and links to source and tests.</Note>

```bash
bunx smithers-orchestrator revert workflow.tsx \
  --run-id RUN_ID --node-id NODE_ID [--attempt N=1] [--iteration N=0]
```

Revert restores files only; it doesn't alter JJ history. The restoration creates a new change on top of the current working copy.

## Requirements

- JJ in `PATH` (`brew install jj` or `cargo install jj-cli`)
- Workspace is a JJ repository (`jj git init` or `jj init`)
- The target attempt was completed when JJ was available (otherwise no pointer was captured)

## Programmatic

```ts
import { revertToAttempt, timeTravel } from "smithers-orchestrator";

const result = await revertToAttempt(adapter, {
  runId: "abc123",
  nodeId: "implement",
  attempt: 2,
  iteration: 0,
});
```

where `adapter` is the `SmithersDb` instance for the run's database.

`revertToAttempt` returns `{ success: false, error: string }` if the attempt is not found or has no recorded JJ pointer (it does not throw for these cases). Inspect the returned `result.success` boolean to detect failures. On success it returns `{ success: true, jjPointer: string }`.

```ts
type RevertOptions = {
  runId: string;
  nodeId: string;
  iteration: number;
  attempt: number;
  onProgress?: (event: SmithersEvent) => void;
};

type RevertResult = {
  success: boolean;
  error?: string;
  jjPointer?: string;
};

function revertToAttempt(adapter: SmithersDb, opts: RevertOptions): Promise<RevertResult>;
```

Use `timeTravel` when you want the higher-level reset flow: it can restore VCS, reset the target node, and reset dependent nodes after the selected attempt.

```ts
const reset = await timeTravel(adapter, {
  runId: "abc123",
  nodeId: "implement",
  attempt: 2,
  iteration: 0,
  resetDependents: true,
  restoreVcs: true,
});
```

```ts
type TimeTravelOptions = {
  runId: string;
  nodeId: string;
  iteration?: number;
  attempt?: number;
  resetDependents?: boolean;
  restoreVcs?: boolean;
  onProgress?: (event: SmithersEvent) => void;
};

type TimeTravelResult = {
  success: boolean;
  jjPointer?: string;
  vcsRestored: boolean;
  resetNodes: string[];
  error?: string;
};

function timeTravel(adapter: SmithersDb, opts: TimeTravelOptions): Promise<TimeTravelResult>;
```

---

## Run State

> The single typed model for "what is this run doing right now?"

Every Smithers surface (CLI, Gateway, and programmatic clients) answers
"what is this run doing right now?" by reading the same `RunStateView`,
computed server-side from persisted state plus liveness signals.

Surfaces never infer status from `ps`, event absence, or partial table
reads. They call `computeRunState`.

## RunState

```ts
type RunState =
  | "running"           // owner alive, work progressing
  | "waiting-approval"  // blocked on a human decision
  | "waiting-event"     // blocked on an external signal
  | "waiting-timer"     // blocked on a scheduled wakeup
  | "paused"            // gracefully paused; resume with `up --resume`
  | "recovering"        // supervisor replaying / resuming
  | "stale"             // owner heartbeat expired, not yet recovered
  | "orphaned"          // owner gone, no supervisor candidate
  | "failed"
  | "cancelled"
  | "succeeded"
  | "unknown"           // telemetry gap; never invent a state
```

When state cannot be determined (e.g. a missing heartbeat with ambiguous DB status), the runtime returns `unknown`. Never invent a more specific state.

The legacy run-row `status` column maps to `RunState` like this:

| `_smithers_runs.status` | `RunState`                     |
| ----------------------- | ------------------------------ |
| `running` (fresh)       | `running`                      |
| `running` (stale)       | `stale` or `orphaned`          |
| `waiting-approval`      | `waiting-approval`             |
| `waiting-event`         | `waiting-event`                |
| `waiting-timer`         | `waiting-timer`                |
| `paused`                | `paused`                       |
| `finished`              | `succeeded`                    |
| `continued`             | `succeeded`                    |
| `failed`                | `failed`                       |
| `cancelled`             | `cancelled`                    |
| missing / unknown text  | `unknown`                      |

`recovering` is reserved for the supervisor takeover window. As of this writing, it is defined but not yet emitted by the runtime.
<Warning>
**`waiting-event` is overloaded; do not assume it always means "send a signal."** The same status covers two situations that need opposite recovery:

1. A genuine pause on a `<Signal>` or `<WaitForEvent>`. Recover by delivering the awaited signal: `bunx smithers-orchestrator signal RUN_ID SIGNAL_NAME --data '{}'`.
2. A run parked with no live event node: an external-trigger / hot-reload / orphan-recovery wait, an aspect or alert human-request, or an approval decided while the run was detached. Here a signal does nothing; the run needs a resume (or `retry-task` / `fork` / answering the human request) after you fix the underlying cause.

Do not guess between them. Ask the runtime, which already disambiguates on
`RunStateView.blocked.kind`:

```bash
bunx smithers-orchestrator why RUN_ID     # names the real blocker and prints the exact unblock command
bunx smithers-orchestrator inspect RUN_ID # includes runState.blocked.kind
```

`runState.blocked.kind === "event"` means a node is truly awaiting an event.
When the run row says `waiting-event` but no node is, `runState.blocked.kind`
is `approval-decided-resume-required` for a detached approval continuation, or
`external-trigger` for a generic parked external wait. `why` still provides the
most detailed unblock command and may report other blockers such as
`retries-exhausted` or `dependency-failed`.
</Warning>
<Warning>
**`succeeded`/`finished` can mask failed child agents.** Run-level status is binary; `finished` or `failed`; and a run is only `failed` when it has an *unhandled* task failure. Two large classes of failed children are deliberately not treated as run-level failures: tasks marked `continueOnFail`, and agent tasks that fail with a *transient* error (rate limits, timeouts, aborts; `SESSION_ERROR`, `TASK_TIMEOUT`, `TASK_HEARTBEAT_TIMEOUT`, `TASK_ABORTED`, or `failureRetryable`). A fan-out where most agents hit provider rate limits can therefore still report `finished` → `succeeded`.

Never trust the top-level status alone to conclude a run truly succeeded. Inspect per-node/agent outcomes:

```bash
bunx smithers-orchestrator inspect RUN_ID   # top-level runState plus every node's state (look for failed nodes)
bunx smithers-orchestrator node NODE_ID      # one node's failure/error, retries, and tool calls
bunx smithers-orchestrator events --run RUN_ID  # the full event history, including per-task failures
bunx smithers-orchestrator scores RUN_ID     # scorer results when you gate on judged output
```

You don't have to re-derive the count by eye. When a `finished` run tolerated at
least one failed child, the masking is surfaced as a first-class **`failedChildren`**
signal in three places:

- **`inspect`** adds `failedChildren` (a count) and `failedChildKeys` (the failed
  task state keys, `nodeId::iteration`) to its JSON next to `runState`, plus a
  `node …` call-to-action. They are absent when nothing failed, so
  `failedChildren > 0` is the degraded-run gate.
- The **`RunResult`** returned by a programmatic run carries the same
  `failedChildren` / `failedChildKeys` fields (omitted when zero).
- The **`RunFinished`** event row records `failedChildren` / `failedChildKeys`, so the
  CLI, Gateway, and DevTools can flag the degraded outcome from the event stream
  without re-reading every node row.

The run-row `status` stays `finished` (and `RunState` stays `succeeded`) so existing
`finished === success` callers and `continueOnFail` semantics are unchanged. The
count is the signal, not a new terminal status.

To avoid the rate-limit failures in the first place, bound fan-out concurrency with `<Parallel maxConcurrency={N}>` so you don't burst past the provider's limit.
</Warning>

## ReasonBlocked / ReasonUnhealthy

`ReasonBlocked` and `ReasonUnhealthy` are optional reason payloads for
waiting and unhealthy states. The type unions are wider than the variants
currently derived from the DB rows.

```ts
type ReasonBlocked =
  | { kind: "approval"; nodeId: string; requestedAt: string }
  | { kind: "event";    nodeId: string; correlationKey: string }
  | { kind: "timer";    nodeId: string; wakeAt: string }
  | { kind: "approval-decided-resume-required"; nodeId: string }
  | { kind: "external-trigger" }
  | { kind: "provider"; nodeId: string; code: "rate-limit" | "auth" | "timeout" }
  | { kind: "tool";     nodeId: string; toolName: string; code: string }

type ReasonUnhealthy =
  | { kind: "engine-heartbeat-stale"; lastHeartbeatAt: string }
  | { kind: "timer-overdue"; wakeAt: string; overdueMs: number }
  | { kind: "ui-heartbeat-stale";     lastSeenAt: string }
  | { kind: "db-lock" }
  | { kind: "sandbox-unreachable" }
  | { kind: "supervisor-backoff"; attempt: number; nextAt: string }
```

Timestamps are ISO-8601 strings. Current `computeRunState` / `deriveRunState`
emits `approval`, `event`, `timer`, `approval-decided-resume-required`, and
`external-trigger` blocked reasons, plus `engine-heartbeat-stale` and
`timer-overdue` unhealthy reasons. The other variants are reserved by the
public type and may appear on future run-state surfaces.

## RunStateView

```ts
type RunStateView = {
  runId: string;
  state: RunState;
  blocked?: ReasonBlocked;
  unhealthy?: ReasonUnhealthy;
  computedAt: string;        // ISO-8601
};
```

`blocked` is present for a `waiting-*` state when `computeRunState` can load
matching pending approval/timer/event context, when a parked `waiting-event`
run has no event-waiting node, or when that context is supplied to
`deriveRunState`. A `waiting-*` state can be returned without `blocked` when
the supporting row is unavailable. `unhealthy` is present for current `stale`,
`orphaned`, and overdue timer results. `recovering` is reserved by the type but
is not currently emitted by the DB derivation. Terminal states (`succeeded`,
`failed`, `cancelled`) carry neither.

## computeRunState

```ts
import { computeRunState } from "@smithers-orchestrator/db/runState";

const view = await computeRunState(adapter, runId);
view.state;       // "running" | ...
view.blocked;     // present for waiting-* only when backing context is found
view.unhealthy;   // present for stale/orphaned heartbeat expiry or overdue timers
```

`computeRunState` is pure over the DB plus the heartbeat / lease signals
on the run row. It does not call `ps`, does not probe sockets, and does
not run heuristics.

`deriveRunState` is the underlying pure function, useful in tests or
when you already have the rows in memory:

```ts
import { deriveRunState } from "@smithers-orchestrator/db/runState";

const view = deriveRunState({
  run,
  pendingApproval,
  pendingTimer,
  pendingEvent,
  now: 1_700_000_000_000,
  staleThresholdMs: 30_000,
});
```

The default `staleThresholdMs` is `30_000`, the same threshold the
engine uses for `isRunHeartbeatFresh`.

## Where it shows up

`RunStateView` is the wire format on every read surface:

- `bunx smithers-orchestrator inspect RUN_ID`: top-level `runState` field on the JSON
  output (and rendered in the human view).
- Gateway RPC `getRun`: `runState` field on the response.
- DevTools snapshot header: `runState?: RunStateView` field.
- Event stream: `RunStatusChanged` records persisted status transitions.
  `RunStateChanged` is a typed/reserved event variant, but the current runtime
  does not emit it; call `getRun` or `computeRunState` when you need the
  derived `RunStateView`.

A run id that does not exist is not a `RunState`. It's an error
(`RUN_NOT_FOUND`). `unknown` is for ambiguity, not for "doesn't exist."

---

## The Smithers Monitor

> A zero-setup live web UI over every run in the workspace, served by the workspace gateway at /monitor and opened with smithers monitor.

The Smithers Monitor is a live browser view over **every run in the workspace**: one page that groups runs by state, streams their execution trees and events, and surfaces every pending approval. It ships inside the CLI and is served by the workspace gateway at `/monitor`, so it works with no `.smithers/` pack installed and no UI code written. It is pure observation: opening it launches no run, no workflow, and no agents.

## 10-second quickstart

```bash
bunx smithers-orchestrator monitor              # open the monitor over all runs
bunx smithers-orchestrator monitor --no-open    # print the URL instead of opening a browser
```

The command resolves the workspace's singleton gateway the same way `bunx smithers-orchestrator ui` does (explicit `--gateway` probe, then runtime-state discovery, then the legacy port probe on `--port`, default 7331) and **autostarts** `smithers gateway` when none is running. Disable autostart with `--no-autostart`; `--no-daemon` or `SMITHERS_NO_DAEMON=1` disables daemonized autostart. It then opens the gateway's `/monitor` page in the browser, deep-linking `?runId=` when you passed a run id, and prints a `{ opened, url, gateway, runId }` envelope.

## What you see

- **Grouped runs list**: needs-attention, active, completed, failed, and cancelled groups with search, status and workflow filters, and live elapsed clocks.
- **Execution tree per run**: the paths through running or failed nodes auto-expand, so the thing you are looking for is already open.
- **Node inspector**: the selected node's output and tool calls.
- **Live event log**: with a follow mode that tracks the newest events.
- **Approvals inbox**: every pending gate across all runs, with approve/deny inline.
- **Run actions**: cancel an active run or resume a failed/cancelled one.
- **A truthful connection badge**: Live when the gateway stream is up, Offline when it is not.
- **Deep links**: `?runId=` and `&nodeId=` address a specific run and node, so URLs are shareable.

## How it relates to the other surfaces

| Surface | What it is for |
| --- | --- |
| `bunx smithers-orchestrator monitor` | Watch **all runs** live in the browser: triage, approvals, and drill-down with zero setup. |
| `bunx smithers-orchestrator ui` | Open **one workflow's own custom UI** (`.smithers/ui/<workflow>.tsx`), purpose-built for that workflow's domain. |
| `bunx smithers-orchestrator ps` / `logs` / `inspect` | The terminal watch loop: scriptable, agent-drivable, works over SSH. See the CLI catalog. |

The monitor replaced the retired `monitor` *workflow*, which had to launch a durable run of agents just to look at another run. The command observes directly through the gateway instead.

---

## Interactive TUI

> Use bunx smithers-orchestrator init, up --interactive, and workflow run --interactive to pick workflows, collect inputs, and watch runs in the full-screen terminal monitor.

Smithers has two terminal UI surfaces:

1. `bunx smithers-orchestrator init` asks one interactive question (your preferred coding agent) unless you pass `--yes` / `--non-interactive`.
2. `bunx smithers-orchestrator up --interactive` and `bunx smithers-orchestrator workflow run --interactive` open the run launcher and then the full-screen monitor.

The old standalone `bunx smithers-orchestrator tui` command was removed in `0.20.2`. The current TUI path is the `--interactive` mode on run commands.

## Interactive setup

Run `init` in a human terminal. It shows which coding agents it detected, asks which one you prefer, installs the workflow pack with defaults plus that agent's plugin (or skill if no plugin exists), and then opens a hijacked tutorial session hosted by that agent:

```bash
bunx smithers-orchestrator init
```

Pass `--agent claude` (or `codex`, `pi`, ...) to skip the question, and `--no-tutorial` to skip the guided session. For CI, scripts, or agent-driven setup, use the non-interactive form:

```bash
bunx smithers-orchestrator init --yes
```

## Interactive run launcher

Use `up --interactive` when you want Smithers to help pick a workflow and collect inputs:

```bash
bunx smithers-orchestrator up --interactive
```

From an interactive TTY, omitting the workflow argument also opens the same launcher:

```bash
bunx smithers-orchestrator up
```

<Frame caption="Captured from tmux: up --interactive opens a searchable workflow picker before it starts a run.">
  <img src="/images/tui/up-interactive-picker.svg" alt="Terminal screenshot of bunx smithers-orchestrator up --interactive showing the workflow picker" />
</Frame>

Use `workflow run --interactive` when you prefer workflow IDs over file paths:

```bash
bunx smithers-orchestrator workflow run --interactive
bunx smithers-orchestrator workflow run WORKFLOW_ID --interactive
```

Passing a workflow ID skips the picker and goes straight to the input prompts for that workflow:

<Frame caption="Captured from tmux: workflow run hello --interactive preselects hello and prompts for its input schema.">
  <img src="/images/tui/workflow-run-interactive-input.svg" alt="Terminal screenshot of workflow run hello --interactive prompting for the name input" />
</Frame>

## Full-screen monitor

After the launcher starts the run, Smithers detaches the workflow process and hands your terminal to the full-screen monitor from `@smithers-orchestrator/tui`. The monitor connects to the workspace Gateway, starts one if needed, and streams the run tree, output, logs, diffs, timeline, and hijack state.

```bash
bunx smithers-orchestrator workflow run hello --interactive
```

<Frame caption="Captured from tmux: after a real hello run finishes, the monitor still shows the run tree and selected node details.">
  <img src="/images/tui/interactive-monitor-hello.svg" alt="Terminal screenshot of the full-screen Smithers TUI monitor showing a finished hello run" />
</Frame>

Useful keys:

| Key | Action |
| --- | --- |
| `1` to `4` | Switch primary tabs |
| `g` | Graph mode |
| `l` | Logs mode |
| `t` | Timeline mode |
| `h` | Hijack mode |
| `?` | Help |
| `q` | Quit the monitor |

There is no silent fallback. If the TUI package cannot be resolved, the command fails with `TUI_MONITOR_UNAVAILABLE` while the detached run keeps running. Watch it with `bunx smithers-orchestrator ps`, `bunx smithers-orchestrator inspect RUN_ID`, or the run log file.

## Command map

| Need | Command |
| --- | --- |
| Interactive setup + tutorial | `bunx smithers-orchestrator init` |
| Non-interactive setup | `bunx smithers-orchestrator init --yes` |
| Pick a workflow and run it | `bunx smithers-orchestrator up --interactive` |
| Pick a workflow ID and run it | `bunx smithers-orchestrator workflow run --interactive` |
| Run a known workflow ID interactively | `bunx smithers-orchestrator workflow run WORKFLOW_ID --interactive` |
| Run a known workflow file interactively | `bunx smithers-orchestrator up .smithers/workflows/hello.tsx --interactive` |
| Run the upgrade workflow with the monitor | `bunx smithers-orchestrator upgrade --interactive` |
| Scripted or CI run | `bunx smithers-orchestrator up WORKFLOW -d` |

Agents should suggest the interactive commands to humans, but should not run them through an automation harness. If an agent is executing a workflow itself, use `-d`, `--format json`, `ps`, `logs`, `inspect`, and `events` instead.

---

## Workflow optimization

> Optimize Smithers workflow prompts against eval suites with GEPA-style prompt artifacts.

Run `bunx smithers-orchestrator optimize` to generate improved prompts for agent tasks via GEPA, verify the improvement against your eval suite, and save the result as a reusable artifact.

```bash
OPENAI_API_KEY=... \
bunx smithers-orchestrator optimize workflow.tsx \
  --cases evals/smoke.jsonl \
  --suite smoke-gepa \
  --artifact .smithers/optimizations/smoke-gepa.json
```

The implicit optimizer is OpenAI-compatible `gpt-5.6-luna` with reasoning
effort `medium`. Pass `--provider` to select Cerebras, Claude, Kimi, or another
supported backend explicitly.

`bunx smithers-orchestrator optimize` runs the eval suite twice:

1. baseline run with the workflow's current prompts
2. optimized run with GEPA-generated prompt patches applied

The command writes the artifact only when the optimized score improves by at least `--min-improvement`. Reports for both runs are written under `.smithers/optimizations/reports` unless `--report-dir` is set.

## Reuse an artifact

Apply the optimized prompts to future evals with `--optimization`:

```bash
bunx smithers-orchestrator eval workflow.tsx \
  --cases evals/smoke.jsonl \
  --suite smoke-optimized \
  --optimization .smithers/optimizations/smoke-gepa.json
```

The artifact patches only agent-backed `<Task>` prompts by `nodeId`. Workflow structure, output schemas, retries, approvals, and persistence behavior stay unchanged.

## Cerebras improvement demo

Example: the following run demonstrates a baseline failure corrected by a GEPA-generated patch. The baseline prompt did not include the required optimization token, so the eval failed. Cerebras GEPA generated a prompt patch that included the missing requirement, and the optimized eval passed.

```bash
CEREBRAS_API_KEY=... bunx smithers-orchestrator optimize workflow.tsx \
  --cases evals/opt.jsonl \
  --suite cerebras-proof \
  --provider cerebras \
  --model zai-glm-4.7 \
  --artifact artifacts/optimized.json \
  --report-dir artifacts/reports \
  --format json
```

Observed result:

```json
{
  "optimization": {
    "schemaVersion": 1,
    "id": "opt-...",
    "strategy": "gepa",
    "optimizer": { "name": "smithers-gepa", "provider": "cerebras", "model": "zai-glm-4.7" },
    "workflowPath": "workflow.tsx",
    "createdAtMs": 0,
    "baseline": { "score": 0.1, "passed": 0, "total": 1 },
    "optimized": { "score": 1, "passed": 1, "total": 1 },
    "improvement": { "absolute": 0.9, "relative": 9 },
    "promptTasks": [],
    "promptPatches": {},
    "reports": { "baseline": "...", "optimized": "..." },
    "artifactPath": "artifacts/optimized.json"
  }
}
```

## Providers

`bunx smithers-orchestrator optimize` accepts the same provider vocabulary Smithers uses for agents and accounts:

| Provider | Optimizer API | Required env | Default model |
| --- | --- | --- | --- |
| `openai-api` (default), `openai`, `openai-sdk`, `codex` | OpenAI-compatible | `OPENAI_API_KEY` | `gpt-5.6-luna` (`medium` reasoning) |
| `cerebras` | OpenAI-compatible | `CEREBRAS_API_KEY` | `zai-glm-4.7` |
| `anthropic-api`, `anthropic`, `anthropic-sdk`, `claude-code`, `claude` | Anthropic Messages API | `ANTHROPIC_API_KEY` | `claude-fable-5` |
| `gemini-api`, `gemini`, `antigravity` | Gemini generateContent API | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | `gemini-3.5-flash` |
| `kimi`, `moonshot` | OpenAI-compatible Moonshot API | `MOONSHOT_API_KEY` | `kimi-k2.7-code` |
| `opencode` | OpenAI-compatible endpoint | `SMITHERS_OPTIMIZER_API_KEY` and `SMITHERS_OPTIMIZER_BASE_URL` | `anthropic/claude-fable-5` |
| `pi` | OpenAI-compatible endpoint | `SMITHERS_OPTIMIZER_API_KEY` and `SMITHERS_OPTIMIZER_BASE_URL` | `gpt-5.6-luna` |
| `amp`, `forge`, `openai-compatible` | OpenAI-compatible endpoint | `SMITHERS_OPTIMIZER_API_KEY` and `SMITHERS_OPTIMIZER_BASE_URL` | pass `--model` when needed |

The default models track the SOTA model registry, which lists the current defaults and badges and is refreshed by a daily research job. The CLI provider names (`codex`, `claude-code`, `antigravity`, `gemini`, `kimi`) map to their hosted API equivalents for optimization because GEPA needs a direct model call to propose prompt patches. Providers with no single hosted backend (`opencode`, `pi`, `amp`, `forge`) are still accepted through a generic OpenAI-compatible endpoint.

Smithers defaults research and prompt-optimization work to Luna. Automatic
workflow routing keeps non-Codex providers behind Codex; this standalone command
does not silently change paid API backends. If OpenAI is unavailable, select a
Cerebras, Claude, Kimi, or other fallback explicitly with `--provider`.

`--provider heuristic` is deterministic and intended for local tests and fixtures. Use `heuristic` when you want deterministic optimization without an API call: place `optimizationHints` in each case's `metadata` to control the patch. It uses eval-case metadata such as:

```json
{
  "metadata": {
    "optimizationHints": {
      "answer": "Include the exact rubric requirement in the task prompt."
    }
  }
}
```

Artifacts are Smithers JSON records with baseline score, optimized score, improvement, prompt patches, and linked eval reports.

---

## SOTA models

> The state-of-the-art model roster smithers configures: descriptions, badges, and role defaults. Registry v3, updated 2026-07-10.

{/* GENERATED FILE. Edit docs/data/sota-models.json, then run `pnpm sota:gen`. */}

**Registry v3** · updated **2026-07-10**. This page is generated from [`docs/data/sota-models.json`](https://github.com/smithersai/smithers/blob/main/docs/data/sota-models.json), the single source of truth for which models smithers configures by default. A daily research job checks every provider for new GA models and opens a PR here when the state of the art moves; `bunx smithers-orchestrator update` picks the changes up on your machine, and re-running `bunx smithers-orchestrator init` refreshes installed workflows to the new defaults.

## Badges

Each badge names the single best model for that job right now.

| Badge | Model | ID |
| --- | --- | --- |
| Best orchestrator | GPT-5.6 Sol | `gpt-5.6-sol` |
| Smartest reviewer | GPT-5.6 Sol | `gpt-5.6-sol` |
| Smartest coder | GPT-5.6 Sol | `gpt-5.6-sol` |
| Fast & cheap | GPT-5.6 Luna | `gpt-5.6-luna` |
| Best value coding | GPT-5.6 Luna | `gpt-5.6-luna` |
| Fastest coding | GPT-5.3-Codex-Spark | `gpt-5.3-codex-spark` |
| Best at UI | Gemini 3.5 Flash | `gemini-3.5-flash` |
| Best open source | Kimi K2.6 | `kimi-k2.6` |

## Role defaults

The Codex-first workflow tiers resolve to these ids; see Recipes for practical workflow patterns:

| Role | Default model |
| --- | --- |
| orchestrator | `gpt-5.6-sol` |
| planning | `gpt-5.6-sol` |
| review | `gpt-5.6-sol` |
| smart | `gpt-5.6-sol` |
| midTier | `gpt-5.6-terra` |
| smartTool | `gpt-5.6-terra` |
| validate | `gpt-5.6-terra` |
| implement | `gpt-5.6-luna` |
| cheapFast | `gpt-5.6-luna` |
| ui | `gpt-5.6-luna` |
| realtime | `gpt-5.3-codex-spark` |
| research | `gpt-5.6-luna` |

### How to choose a tier

The role table is a starting policy, not a claim that every task belongs to one model. [OpenAI's GPT-5.6 release](https://openai.com/index/gpt-5-6/) describes a three-tier family: Sol is the flagship, Terra is balanced, and Luna is the fastest and most affordable. The release materials describe the family across software engineering, knowledge work, research, computer use, and tool use. In Smithers, that supports using Luna much more often than a narrow cheap-implementation-only policy, while still escalating when the decision warrants it.

| Situation | Start with | Escalate when |
| --- | --- | --- |
| Research, implementation, ordinary tool use, UI work, or routine-to-substantial execution | `gpt-5.6-luna` | The task is ambiguous, high-stakes, repeatedly failing, or needs a final frontier-level judgment |
| Explicit tool-heavy validation, structured checking, or higher-judgment mid-tier escalation | `gpt-5.6-terra` | The check itself requires difficult decomposition, novel judgment, or a final ship/no-ship decision |
| Orchestration, planning, final review, novel architecture, or the hardest reasoning | `gpt-5.6-sol` | Usually do not escalate further; use a human gate for decisions that require human authority |
| No usable Codex authentication for Sol-backed orchestration, planning, review, or smart work | `claude-fable-5` | Use the provider's supported fallback (Opus, then Kimi) or an explicit Claude-native specialist choice |
| No usable Codex authentication for Luna/Terra-backed research, implementation, validation, mid-tier, or tool-heavy work | `claude-sonnet-5` | Use the provider's supported fallback (Kimi) or an explicit Claude-native specialist choice |

Use Luna first, including ordinary tool use, then escalate deliberately: let Luna gather sources, edit code, use ordinary tools, and produce a substantive first result; use the `smartTool` role only when the work is explicitly tool-heavy and needs Terra's validation or higher judgment, and use Sol for planning and review when the decision warrants it. Agent arrays are sequential fallback chains: a later provider is attempted only when an earlier one is unavailable or fails. An explicitly constructed provider agent is a deliberate choice and can run even when Codex is healthy.

```tsx
const researcher = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });
const implementer = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });
const validator = new CodexAgent({ model: "gpt-5.6-terra" });
const reviewer = new CodexAgent({ model: "gpt-5.6-sol" });
const fableFallback = new ClaudeCodeAgent({ model: "claude-fable-5" });
const smartFallbackChain = [reviewer, fableFallback];
```

Fable 5 is not a Codex tier. Anthropic describes it as a model for ambitious, long, complex knowledge and coding work; developers can use `claude-fable-5` through the Claude API at $10 per million input tokens and $50 per million output tokens. Anthropic's July 1 redeployment restored access globally on Claude Platform, Claude.ai, Claude Code, and Claude Cowork. For Pro, Max, Team, and select Enterprise plans, Anthropic said Fable 5 would be included for up to 50% of weekly usage limits through July 12, 2026 at 11:59:59 PM PT ([promotion details](https://support.claude.com/en/articles/15424964-claude-fable-5-promotional-access)), after which use requires usage credits; standard Enterprise has no included allowance. Anthropic said it would re-enable AWS, Google Cloud, and Microsoft Foundry as quickly as possible, while its current Fable page lists availability through available marketplaces and those cloud platforms, so cloud-provider and account availability should be treated as conditional rather than universal. Its updated safeguards can reroute blocked requests to Opus 4.8 and can false-positive more often during routine coding and debugging. In current Claude applications and Claude Code behavior, that Fable-to-Opus switching is enabled by default, while API customers must opt in and configure the fallback themselves ([default product behavior and API opt-in](https://support.claude.com/en/articles/15363606-why-claude-switched-models-in-your-conversation-with-fable-5)). Treat this as current product behavior, not a permanent provider guarantee. Smithers therefore keeps Fable as a non-Codex fallback for Sol-backed smart, planning, review, and orchestration roles, or a deliberate Claude-native specialist when those cost, availability, and safeguard tradeoffs fit; Luna/Terra-backed research, implementation, validation, mid-tier, and tool-heavy roles fall back to Sonnet instead, not Fable; the cited sources do not establish a general latency comparison. Automatic fallback pools do not silently run Fable in parallel with a healthy Codex path; an explicit Claude/OpenCode workflow may choose it directly. In Claude Code the model id is `claude-fable-5`; the OpenCode configuration uses the provider-qualified id `anthropic/claude-fable-5`. See [Anthropic's Fable 5 announcement](https://www.anthropic.com/news/claude-fable-5-mythos-5) and [July redeployment update](https://www.anthropic.com/news/redeploying-fable-5).

## Anthropic

### Claude Fable 5 (`claude-fable-5`)

current · engines: `claude`, `opencode` · roles: orchestrator, planning, review, smart

Anthropic's Mythos-class model for ambitious, long, complex knowledge and coding work. Developers can use `claude-fable-5` through the Claude API at $10 per million input tokens and $50 per million output tokens. Anthropic's July 1 redeployment restored access on Claude Platform, Claude.ai, Claude Code, and Claude Cowork; the temporary included window for eligible subscription plans runs through July 12, 2026 at 11:59:59 PM PT (promotion details), and cloud-platform availability remains subject to the marketplace, provider, and account. Its updated safeguards may reroute blocked requests to Opus 4.8 and may false-positive during routine coding and debugging. Current Claude applications and Claude Code enable that switching by default, while API customers must opt in and configure it (default product behavior and API opt-in); treat the notice as current product behavior rather than a permanent provider guarantee. Smithers uses Fable as a non-Codex fallback for Sol-backed smart, planning, review, and orchestration roles or as a deliberate Claude-native specialist. Claude Code uses `claude-fable-5`; Smithers' OpenCode configuration uses the provider-qualified id `anthropic/claude-fable-5`. See Anthropic's launch announcement, redeployment update, and [current Fable availability](https://www.anthropic.com/claude/fable).

### Claude Opus 4.8 (`claude-opus-4-8`)

current · engines: `claude`, `opencode` · roles: smart

Anthropic's Opus tier. Still a top-shelf generalist and the always-present fallback behind Fable in the smart, planning, and review pools.

### Claude Sonnet 5 (`claude-sonnet-5`)

current · released 2026-06-29 · engines: `claude`, `opencode` · roles: implement, cheapFast

The newest Sonnet: fast, cheap, 1M context. It is the primary non-Codex fallback for Luna/Terra-backed research, implementation, validation, mid-tier, and tool-heavy work when Codex is unavailable; Sol-backed orchestration, planning, review, and smart work fall back to Fable instead.

### Claude Haiku 4.5 (`claude-haiku-4-5`)

current · engines: `claude`, `opencode`

Anthropic's cheapest tier, for high-volume summarization and classification where even Sonnet is overkill.

## OpenAI

### GPT-5.6 Sol (`gpt-5.6-sol`)

**Best orchestrator** · **Smartest reviewer** · **Smartest coder** · state of the art · engines: `codex`, `pi` · roles: orchestrator, planning, review, smart

OpenAI's GPT-5.6 flagship for the hardest reasoning, coding, and judgment-heavy work. Keep Sol for orchestration, planning, final review, ambiguous or high-stakes decisions, and recovery when a lower-cost pass is not enough; do not spend it on routine implementation by default. See OpenAI's GPT-5.6 release.

### GPT-5.6 Terra (`gpt-5.6-terra`)

state of the art · engines: `codex`, `pi` · roles: midTier, smartTool, validate

OpenAI's balanced GPT-5.6 model for validation and higher-judgment, explicitly tool-heavy work. Terra is the `smartTool` escalation when a routine Luna pass needs more checking or consistency but the task does not need Sol's top-tier planning and review; ordinary tool use starts with Luna. See OpenAI's GPT-5.6 release.

### GPT-5.6 Luna (`gpt-5.6-luna`)

**Fast & cheap** · **Best value coding** · state of the art · engines: `codex`, `pi` · roles: implement, cheapFast, research, ui

OpenAI's fastest and most affordable GPT-5.6 tier, with strong capability across the family’s software, knowledge-work, research, computer-use, and tool-using workloads. Ordinary tool use starts with Luna, which is the default for research, implementation, routine-to-substantial execution, UI work, cheapFast tasks, and the first pass on many tasks that do not require Sol's judgment. Escalate to Terra only for explicit tool-heavy validation or higher-judgment checking; escalate to Sol for ambiguity, high stakes, repeated failure, or unusually long-horizon reasoning rather than assuming task size alone requires Sol. See OpenAI's GPT-5.6 release and the [GPT-5.6 preview release](https://openai.com/index/previewing-gpt-5-6-sol/).

### GPT-5.5 (`gpt-5.5`)

current · engines: `codex`, `pi`

The previous OpenAI flagship. Keep it as a compatibility fallback for Codex accounts that do not yet expose GPT-5.6.

### GPT-5.4 (`gpt-5.4`)

current · engines: `codex`, `pi`

The previous OpenAI flagship, retained for pinned compatibility. New Smithers workflows use the GPT-5.6 Sol/Terra/Luna role split instead.

### GPT-5.4 mini (`gpt-5.4-mini`)

current · engines: `codex`, `pi`

OpenAI's prior fast, efficient tier. Retained as the OpenRouter compatibility fallback (openai/gpt-5.4-mini); Codex-native cheap work defaults to GPT-5.6 Luna.

### GPT-5.3-Codex-Spark (`gpt-5.3-codex-spark`)

**Fastest coding** · state of the art · released 2026-02-12 · engines: `codex` · roles: realtime

OpenAI's first real-time coding model: 1000+ tokens per second on dedicated Cerebras hardware while staying genuinely capable. Research preview, ChatGPT Pro only. Reach for it when iteration latency matters more than depth.

## Google

### Gemini 3.5 Flash (`gemini-3.5-flash`)

**Best at UI** · state of the art · released 2026-05-19 · engines: `antigravity` · roles: ui, implement, cheapFast

Google's best price-to-performance model and the best non-Codex fallback for UI work: near-Pro intelligence at Flash speed and cost, 1M context, and it beats Gemini 3.1 Pro on coding and agentic benchmarks while running roughly 4x faster. Smithers can use it for UI work when its provider-specific route is configured; it is not a universal fallback policy.

### Gemini 3.1 Pro (`gemini-3.1-pro-preview`)

current · engines: `antigravity`

Google's Pro tier (still preview). Superseded for coding and agentic work by Gemini 3.5 Flash; keep it for long-horizon reasoning where Pro depth wins.

### Gemini 3.1 Flash-Lite (`gemini-3.1-flash-lite`)

current · engines: `antigravity`

Frontier-class performance at a fraction of the cost. The floor of the Gemini lineup for bulk, low-stakes calls.

## Moonshot AI

### Kimi K2.7-Code (`kimi-k2.7-code`)

state of the art · released 2026-06-12 · engines: `kimi` · roles: implement, cheapFast

Moonshot's most capable coding model: 256k context, roughly 30% fewer reasoning tokens than K2.6, and strong instruction-following in long contexts. Smithers keeps it as a no-Codex implementation fallback; the -highspeed variant trades a little quality for ~180 tok/s.

### Kimi K2.7-Code High-Speed (`kimi-k2.7-code-highspeed`)

current · engines: `kimi` · roles: realtime

The high-throughput K2.7-Code variant, around 180 tokens per second. The open-weights answer to Codex-Spark for latency-sensitive loops.

### Kimi K2.6 (`kimi-k2.6`)

**Best open source** · current · released 2026-04-20 · engines: `kimi` · roles: research

Moonshot's open-source (modified MIT) trillion-parameter MoE flagship: native multimodal, agent swarms up to 300 sub-agents, up to 13 hours of continuous coding. Ties GPT-5.5 on SWE-Bench Pro at roughly 80% lower cost. The strongest model you can self-host.

## Benchmarks

Independent of the roster above: how these models actually score. smithers ships two benchmark harnesses in-repo: [`benchmarks/roadmapbench`](https://github.com/smithersai/smithers/tree/main/benchmarks/roadmapbench) (RoadmapBench, a real audited run) and [`benchmarks/site`](https://github.com/smithersai/smithers/tree/main/benchmarks/site) (the benchmarks.smithers.sh leaderboard, sourced from `benchmarks/results.json`). Only results for models that still appear in the registry above are shown; rows for older models (e.g. Claude Opus 4.6, GPT-5.2) are dropped automatically. `pending` rows are honest placeholders: the smithers fleet run has not completed at full scale yet.

{/* Generated from docs/data/sota-benchmarks.json. Rows naming a deprecated or unlisted model are filtered out. */}

### RoadmapBench

[UniPat-AI/RoadmapBench (115 tasks)](https://github.com/UniPat-AI/RoadmapBench) · metric: Resolved Rate (RR) · Completion Score (CS) · harness: `benchmarks/roadmapbench`

Long-horizon, multi-target version upgrades: start from a repo pinned to an old release and implement every independent feature a real upstream bump introduced (median oracle change ~3,700 LOC across ~51 files; median 5 scored targets). The best single agent on the official leaderboard resolves under 40% of tasks.

| Models | Harness | Score | Sample | Notes |
| --- | --- | --- | --- | --- |
| Claude Opus 4.8 + GPT-5.5 | smithers (fable-sandwich) | **RR 0.50 · CS 0.857** | curated subset (Python + TypeScript) · n=2 | every grader fair-validated (oracle=1.0, no-op<1.0); every run audited untainted. Not a full-115 claim. |
| public SOTA (best single agent) | official | RR ~0.39 · CS ~0.69 | full-115 · n=115 | approximate published best on the official leaderboard |

### SWE-Bench Pro

[ScaleAI/SWE-bench_Pro (test, 731 tasks)](https://github.com/scaleapi/SWE-bench_Pro-os) · metric: Pass@1 (%) · harness: `benchmarks/site`

731 multi-file tasks across 11 professional repos (Go/Python/JS/TS) graded by hidden tests in canonical per-instance Docker. Moonshot reports Kimi K2.6 tying GPT-5.5 here at roughly 80% lower cost.

| Models | Harness | Score | Sample | Notes |
| --- | --- | --- | --- | --- |
| Claude Opus 4.8 + Claude Sonnet 5 | smithers (delegation-chain) | pending | full-731 | Claude-only fleet run pending |

### Claw-Eval-Live

[Claw-Eval-Live (105 tasks / 17 families)](https://arxiv.org/abs/2604.28139) · metric: Score (pass bar 75) · harness: `benchmarks/site`

Agentic enterprise workflows scored on observable execution evidence. No public model clears the 70% pass bar; frontier flagships cluster in the low 80s.

| Models | Harness | Score | Sample | Notes |
| --- | --- | --- | --- | --- |
| GPT-5.4 | official | 81.7 | full-105 · n=105 | public leaderboard |
| smithers (mixture) | mixture-gateway | pending | pending | cannot run faithfully Claude-only (native OpenAI gather leg + neutral Gemini judge); asterisked variant only |

### SWE-EVO

[SWE-EVO (48 release-sized tasks / 7 repos)](https://arxiv.org/abs/2512.18470) · metric: Resolved Rate (%) · harness: `benchmarks/site`

Carry a Python project across a whole release transition. Frontier agents resolve only ~21-25% of the full-48 set.

| Models | Harness | Score | Sample | Notes |
| --- | --- | --- | --- | --- |
| GPT-5.4 | official (OpenHands) | 25.0 | full-48 · n=48 | Wilson 95% CI [14.9, 38.8] |
| Claude Opus 4.8 + Claude Sonnet 5 | smithers (delegation-chain) | pending | dvc subset | Claude-only fleet run pending; dvc subset is easier than full-48, not directly comparable |

## Deprecated ids

Rewrite these on sight; the daily research job does the same sweep mechanically.

| Deprecated | Use instead |
| --- | --- |
| `claude-sonnet-4-6` | `claude-sonnet-5` |
| `claude-sonnet-4-7` | `claude-sonnet-5` |
| `claude-sonnet-4-20250514` | `claude-sonnet-5` |
| `gpt-5.3-codex` | `gpt-5.6-luna` |
| `gpt-5.2` | `gpt-5.6-luna` |
| `gpt-4o` | `gpt-5.4-mini` |
| `kimi-latest` | `kimi-k2.7-code` |

## Update policy

- Only models with exact usable ids confirmed by provider documentation enter the default registry; marketing-only names and previews without usable ids wait.
- Research previews with real availability (e.g. gpt-5.3-codex-spark on ChatGPT Pro) may enter with status sota for their niche, noted in the description.
- No floating aliases (kimi-latest, *-latest). Pin concrete ids so a provider-side model swap cannot bypass this registry.
- Every badge names exactly one model. A new badge holder demotes the old one in the same change.
- Any change to models bumps version by exactly 1 and refreshes updatedAt.
- Deprecated entries keep their replacedBy id for one release so sweeps know what to rewrite, then get deleted.


===============================================================================

# Smithers Memory

> Smithers cross-run memory: working memory, message history, semantic recall, processors.

---

## Memory

> Cross-run facts, message history, namespaces, and task memory metadata.

Memory persists state **across runs**. Task outputs are per-run; memory is per-namespace and survives every workflow execution.

<Note>API reference: Memory lists every memory export and type, its options, and links to source and tests.</Note>

```mermaid
flowchart LR
  W[workflow] --> ST
  AG[agent] --> ST
  U[user] --> ST
  GL[global] --> ST
  ST[(Memory store<br/>one SQLite db)] --> F[Facts · namespaced JSON, optional TTL]
  ST --> H[Message history · ordered threads]
  ST --> N[Notes · append-only knowledge, supersession + status gate]
  style ST fill:#def,stroke:#36c
```

## Four layers

```ts
import { createMemoryStore } from "smithers-orchestrator/memory";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";

const sqlite = new Database("smithers.db");
const db = drizzle(sqlite);
const store = createMemoryStore(db);
const ns = { kind: "workflow" as const, id: "code-review" };
```

| Layer | API | Use for |
|---|---|---|
| Facts | `store.setFact(ns, key, value, ttlMs?)` / `store.getFact(ns, key)` / `store.listFacts(ns)` / `store.listAllFacts()` | Namespaced JSON facts. Optional TTL. Last-write-wins. `listAllFacts()` returns every fact across all namespaces (what `memory list` prints with no namespace). |
| Message history | `store.createThread(ns, title?)`, `store.listThreads()`, `store.saveMessage(msg)`, `store.listMessages(threadId, limit)`, `store.deleteMessages(threadId, messageIds)` | Ordered chat threads per agent or user; list/delete to compact history. |
| Notes | `store.saveNote(input)` / `store.getNote(id)` / `store.listNotes(ns, filter?)` / `store.setNoteStatus(id, status)` / `store.enableNoteSearch(kind)` / `store.searchNotes(kind, query, limit?, filter?)` | Append-only knowledge (lessons, decisions, runbook rules). Notes never mutate; they die by supersession or rejection, not TTL. SQLite backend only. |
| Maintenance | `store.deleteExpiredFacts()` plus processors | TTL cleanup and history compaction. |

Every method has an Effect-returning twin (`store.listThreadsEffect`, `store.deleteMessagesEffect`, etc.) for use inside an Effect pipeline.

## Namespaces

```ts
type MemoryNamespace = { kind: "workflow" | "agent" | "user" | "global"; id: string };
```

Pick the kind to match the lifetime: `workflow` is scoped to a workflow definition; `agent` to an agent identity; `user` to an end user; `global` is shared across everything.

## Task Memory Metadata

```tsx
<Task
  id="review"
  output={outputs.review}
  agent={reviewer}
  memory={{
    recall:   { namespace: ns, topK: 3 },               // inject relevant past facts into prompt
    remember: { namespace: ns, key: "last-review" },    // persist this output as a fact
    threadId: `${ctx.input.repo}:reviews`,              // append messages to this thread
  }}
>
  Review the latest PR.
</Task>
```

`memory` is preserved on the task descriptor as metadata for runtimes and integrations that layer memory behavior onto task execution. The direct store APIs above are the current public memory read/write surface.
## Imperative get/set/delete inside a workflow

The `memory={{ recall, remember }}` block above is **declarative metadata**; it is preserved on the task descriptor for runtimes that layer memory onto task execution, not an imperative call. To actually get, set, or delete a fact **while a run is executing**, build a store with `createMemoryStore(db)` and call it from inside a compute `<Task>` (a function task with no `agent`). The compute callback receives `deps` only; there is no injected `store`, so you create one:

```tsx
import { createMemoryStore } from "smithers-orchestrator/memory";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";

const store = createMemoryStore(drizzle(new Database("smithers.db")));
const ns = { kind: "workflow" as const, id: "code-review" };

<Task id="remember-model" output={outputs.done}>
  {async () => {
    await store.setFact(ns, "model", "gpt-4");           // set
    const fact = await store.getFact(ns, "model");        // get (MemoryFact | undefined)
    const all  = await store.listFacts(ns);               // list
    await store.deleteFact(ns, "stale-key");              // delete
    return { done: true };
  }}
</Task>
```

Create the store once at module scope (outside the workflow) and reuse it across tasks rather than re-opening the SQLite handle in every task body. Under Effect, the same operations are available as `store.setFactEffect(ns, key, value, ttlMs?)`, `store.getFactEffect(ns, key)`, etc., or via `MemoryService` (`MemoryServiceApi`), which also exposes the underlying `.store`.


## Durable notes

Facts are a mutable scratch lane; **notes** are the durable knowledge lane.
A note's body, labels, and provenance never change after insert. Knowledge
evolves by **supersession**: a new note lists the ids it replaces, and the
superseded notes drop out of default reads only once the superseder is
**accepted**. `status` is the one mutable field: a human or workflow gate
flips it with `setNoteStatus`, so propose-then-reject leaves the original
knowledge untouched.

The **default read contract** (no filter) is a stability contract: reads
return notes that are accepted and not superseded by an accepted note.
Filters (`status`, `includeSuperseded`, `kind`, `namespace`) widen or narrow.

```ts
const ops = { kind: "user" as const, id: "ops-team" };

// Bank a lesson (append-only; provenance records which run learned it).
const lesson = await store.saveNote({
  namespace: ops,
  body: "Warm the cache tier before scaling deploys back up.",
  kind: "lesson",
  provenance: { runId: ctx.runId, nodeId: "bank" },
});

// Propose ONE rule that replaces piled-up lessons; pending hides nothing.
const rule = await store.saveNote({
  namespace: ops,
  body: "Always pre-warm caches after scale-down.",
  kind: "runbook-rule",
  status: "pending",
  supersedes: [lesson.id],
});

// The human gate: accepting the rule hides the lessons it replaces.
await store.setNoteStatus(rule.id, "accepted");

// Recall live knowledge only (accepted, not superseded).
const live = await store.listNotes(ops);
```

Full-text search is **lazy and opt-in per namespace kind**: nothing is
indexed (and note writes pay nothing) until `enableNoteSearch(kind)`, which
creates the FTS index and backfills. `searchNotes(kind, query)` spans every
namespace of the kind; pass `{ namespace }` in the filter to stay
namespace-local on a shared database. Notes require the SQLite backend and
fail loud on Postgres/PGlite. See
[`examples/incident-runbook-memory.jsx`](https://github.com/smithersai/smithers/blob/main/examples/incident-runbook-memory.jsx)
for the full recall → triage → bank → distill → ratify loop.

## Processors

Maintenance jobs you run periodically:

```ts
import { TtlGarbageCollector, TokenLimiter, Summarizer } from "smithers-orchestrator/memory";

const gc         = TtlGarbageCollector();   // expire facts past their TTL
const limiter    = TokenLimiter(4000);      // keep history under token budget
const summarizer = Summarizer(myAgent);     // compress old messages with an LLM

await gc.process(store);
await limiter.process(store);
await summarizer.process(store);
```

```mermaid
flowchart LR
  M[Facts + messages accumulate] --> GC[TtlGarbageCollector<br/>drop expired facts]
  GC --> TL[TokenLimiter<br/>trim history to budget]
  TL --> SM[Summarizer<br/>compress old messages with an LLM]
  SM --> K[Compact, in-budget memory]
  style K fill:#dfe,stroke:#3a3
```

## Inspect from the CLI

```bash
bunx smithers-orchestrator memory list workflow:code-review -w workflow.tsx
```

The CLI currently exposes fact listing. Use the store API for writes, deletes, threads, messages, and TTL cleanup.

## Notes

- Memory and task outputs are **distinct stores**. Don't use memory for run-scoped state; it's not transactional with the workflow's frame commits.
- Working-memory writes are unordered. Use message history when sequence matters.

---

## Memory Quickstart

> Folded into the opt-in memory fragment.

This material is now in the opt-in `/llms-memory.txt` fragment.


===============================================================================

# Smithers OpenAPI Tools

> Smithers OpenAPI tools: turn an OpenAPI spec into AI SDK tools, with auth, filters, and observability.

---

## OpenAPI Tools

> Generate AI SDK tools from an OpenAPI spec. Auth, filtering, observability built in.

`createOpenApiTools` parses an OpenAPI 3.x spec and returns AI SDK tools, one per operation, with Zod schemas converted from the spec's JSON schemas.

<Note>API reference: OpenAPI lists every OpenAPI export and option, and links to source and tests.</Note>

```ts
import { ToolLoopAgent } from "ai";
import { openai } from "@ai-sdk/openai";
import { createOpenApiTools } from "smithers-orchestrator/openapi";

const tools = await createOpenApiTools("./petstore.json", {
  baseUrl: "https://api.petstore.example.com",
  auth: { type: "bearer", token: process.env.PETSTORE_TOKEN! },
});

const agent = new ToolLoopAgent({
  model: openai("gpt-5.6-terra"),
  tools,
});
```

## Options

```ts
type OpenApiToolsOptions = {
  baseUrl?: string;                  // overrides spec.servers[0].url
  headers?: Record<string, string>;  // merged into every request
  auth?:
    | { type: "apiKey"; name: string; in: "header" | "query"; value: string }
    | { type: "bearer"; token: string }
    | { type: "basic"; username: string; password: string };
  include?: string[];                // operationId allowlist
  exclude?: string[];                // operationId blocklist
  namePrefix?: string;               // prefix generated tool names
  operations?: Record<
    string,
    | false
    | {
        include?: boolean;           // false skips this operation
        name?: string;               // agent-facing tool name
        description?: string;        // agent-facing tool description
        responseExamples?: Array<{
          status?: string | number;
          description?: string;
          value: unknown;
        }>;
      }
  >;
};
```

Pass the spec itself as the first argument to `createOpenApiTools(input, options)`. `input` may be a parsed spec object, file path, URL, or raw OpenAPI text.

## Public API

| Export | Purpose |
|---|---|
| `createOpenApiTools(input, options?)` / `createOpenApiToolsSync(input, options?)` | Build a record of AI SDK tools keyed by operationId. |
| `createOpenApiTool(input, operationId, options?)` / `createOpenApiToolSync(...)` | Build one operation as a tool. |
| `listOperations(input)` | Return parsed operation metadata without creating tools. |
| `extractOperations(spec)` | Extract operations from an already parsed spec. |
| `loadSpecEffect(input)` | Load and parse a spec from object, path, URL, or raw text. |
| `loadSpecSync(input)` | Load and parse a spec from object, local file path, or raw text. It does not fetch URLs. |
| `jsonSchemaToZod(schema, spec, visited?)` / `buildOperationSchema(parameters, requestBody, spec)` | Lower-level schema conversion helpers. |

## CLI

Generate a reusable AI SDK tools module:

```bash
bunx smithers-orchestrator openapi generate ./api/openapi.yaml ./src/openapi-tools.js
```

The generated module imports `createOpenApiToolsSync`, creates the tools from the spec, and exports them as both `tools` and the default export.

```bash
bunx smithers-orchestrator openapi list ./api/openapi.yaml
```

`openapi list` previews every operationId, method, path, and summary. Useful for auditing what an agent will be able to call before wiring it up.

## What gets generated

For each operation:

- A Zod schema for the request body + path/query/header parameters.
- An `execute(args)` function that performs the HTTP call.
- The operation's `summary` / `description` becomes the tool description.

## Response handling

- JSON responses are parsed and returned as objects.
- Non-JSON responses are returned as strings.
- HTTP status codes are not special-cased by the tool factory; if the server returns a JSON or text error body, that body is returned to the agent.
- Request failures, fetch failures, and schema/tool execution exceptions are returned as `{ error: true, message, status: "failed" }` so the agent can react in its loop.

## Filtering

`include` / `exclude` accept arrays of `operationId`. If both are set, exclude wins. Useful for limiting an agent's surface area to a specific feature ("just the inventory endpoints"). `namePrefix` prefixes generated tool names without changing the operationIds used for filtering.

Use `operations` for per-operation curation keyed by the original `operationId`. Set an entry to `false` or `{ include: false }` to skip that endpoint, set `name` to rename the generated tool, set `description` to replace the spec summary/description with agent-facing instructions, and add `responseExamples` to append concrete response shapes to the tool description. This lets a connector expose a small curated set instead of dumping every endpoint as an agent-callable tool. `include` / `exclude` still use the original `operationId`, not the curated name.

## Observability

OpenAPI tool calls update the exported Effect metrics (`openApiToolCallsTotal`, `openApiToolCallErrorsTotal`, `openApiToolDuration`) and carry Effect log annotations/spans with the operation id, method, and path. The current tool factory does not emit `OpenApiToolCalled` onto the Smithers run event bus; that event variant is typed/categorized for future event-bus integration.

## Notes / Limitations

- Schema composition (`allOf`, `anyOf`, `oneOf`) is supported; converts to Zod unions/intersections.
- Nullable fields and defaults from the spec are preserved.
- Cookie parameters are ignored when generating tool input schemas and requests.
- JSON request bodies are the only request body media type generated today; non-JSON bodies such as form data, multipart uploads, and binary payloads are not encoded.
- Parameter serialization styles are not implemented. Path parameters are URL-encoded and substituted directly, and query parameters are sent with default `URLSearchParams` serialization.
- Swagger 2.0 objects are not supported as a first-class input format. Use OpenAPI 3.x specs with `openapi`, `paths`, and `info` fields.

---

## OpenAPI Tools Quickstart

> Folded into the opt-in OpenAPI fragment.

This material is now in the opt-in `/llms-openapi.txt` fragment.


===============================================================================

# Smithers Observability

> Smithers observability surface: HTTP server, gateway, MCP, OpenTelemetry, metrics.

---

## HTTP Server

> Run Smithers as an HTTP server: REST routes for runs, approvals, tools.

`startServer` boots a multi-workflow HTTP server with REST endpoints for run lifecycle, SSE event streams, and human-in-the-loop approvals. For a single-workflow variant alongside `bunx smithers-orchestrator up`, see Serve Mode.

<Note>API reference: Server & Gateway lists every server and gateway export, its options, and links to source and tests.</Note>

## Quick start

```ts
import { startServer } from "smithers-orchestrator";
import { drizzle } from "drizzle-orm/bun-sqlite";

const server = startServer({
  port: 7331,
  db: drizzle("./smithers.db"),
  authToken: process.env.SMITHERS_API_KEY,
  rootDir: process.cwd(),
});
```

```bash
curl -X POST http://localhost:7331/v1/runs \
  -H "Authorization: Bearer $SMITHERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflowPath": "./bugfix.tsx", "input": {"description": "fix auth"}}'
```

## ServerOptions

```ts
type ServerOptions = {
  port?: number;              // default 7331
  host?: string;              // default 127.0.0.1; a non-loopback bind requires authToken
  db?: unknown;               // enables GET /v1/runs and approvals listing
  authToken?: string;         // falls back to process.env.SMITHERS_API_KEY
  insecure?: boolean;         // default false; allow a non-loopback bind with no authToken (dangerous)
  maxBodyBytes?: number;      // default 1_048_576
  rootDir?: string;           // sandbox root for workflow paths and tools
  allowNetwork?: boolean;     // default false; allows network in `bash`
  integrations?: IntegrationsConfig; // external HMAC-verified integrations served by this process (requires db)
  headersTimeout?: number;    // default 30_000
  requestTimeout?: number;    // default 60_000
};
```

`startServer` returns a listening `http.Server`. `headersTimeout` and `requestTimeout` are applied to that server to bound slow header/body uploads.

## API Routes (TOON)

```toon
routes[15]{method,path,purpose,auth}:
  GET,/metrics,Prometheus exposition,bearer
  POST,/v1/runs,Start or resume a run,bearer
  GET,/v1/runs,List runs (requires db),bearer
  GET,/v1/runs/:runId,Run status and node summary,bearer
  POST,/v1/runs/:runId/resume,Resume paused or failed run,bearer
  POST,/v1/runs/:runId/cancel,Abort an active run,bearer
  GET,/v1/runs/:runId/events,SSE event stream (?afterSeq=N),bearer
  GET,/v1/runs/:runId/frames,List render frames,bearer
  POST,/v1/runs/:runId/nodes/:nodeId/approve,Approve a paused node,bearer
  POST,/v1/runs/:runId/nodes/:nodeId/deny,Deny a paused node,bearer
  POST,/v1/runs/:runId/signals/:signalName,Deliver a named signal,bearer
  GET,/v1/approvals,List pending approvals (requires db),bearer
  GET,/v1/approval/list,Legacy alias for /v1/approvals,bearer
  GET,/approval/list and /approvals,Legacy aliases for /v1/approvals,bearer
  POST,/signal/:runId/:signalName,Legacy alias for signals,bearer
```

JSON requests/responses use `Content-Type: application/json`, `Cache-Control: no-store`, and `X-Content-Type-Options: nosniff`. SSE events are named `smithers` and carry `SmithersEvent` JSON; the stream sends a keep-alive comment every 10 s and closes on terminal state.

Errors use the envelope `{ "error": { "code", "message", "details" } }`. Common codes: `INVALID_REQUEST`, `INVALID_JSON`, `PAYLOAD_TOO_LARGE`, `RUN_ID_REQUIRED`, `RUN_NOT_FOUND`, `RUN_ALREADY_EXISTS`, `RUN_NOT_ACTIVE`, `NOT_FOUND`, `UNAUTHORIZED`, `WORKFLOW_PATH_OUTSIDE_ROOT`, `DB_NOT_CONFIGURED`, `SERVER_ERROR`.

## Tool surface

Tools resolve relative to `rootDir`. The example below exposes a workflow that uses the built-in `bash` tool through the server; clients call it via `POST /v1/runs`.

```tsx
import { Task, Workflow, createSmithers } from "smithers-orchestrator";
import { bashTool } from "smithers-orchestrator/tools";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  result: z.object({ stdout: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="echo">
    <Task id="run" output={outputs.result}>
      {async () => ({ stdout: await bashTool("echo", [ctx.input.msg]) })}
    </Task>
  </Workflow>
));
```

`allowNetwork: false` (the default) keeps `bash` offline. Set `rootDir` to constrain the filesystem the workflow can touch.

## Authentication

When `authToken` is configured (directly or via `SMITHERS_API_KEY`), every request must include either:

- `Authorization: Bearer <token>`, or
- `x-smithers-key: <token>`.

Missing or invalid tokens return `401`. No scopes; for finer access control use the Gateway.

## Notes

- Each `POST /v1/runs` and `/resume` reloads the workflow source via a content-addressed shadow file with the same extension as the source, so edits take effect on the next call without a restart.
- Active runs heartbeat to `_smithers_runs.heartbeat_at_ms` every 1 s; stale rows (no heartbeat in 30 s) are treated as crashed and may be resumed.
- When a server-level `db` differs from a workflow's database, runs and events are mirrored asynchronously to the server db so they show up in `GET /v1/runs`.
- Metrics are exported via `/metrics`; set `SMITHERS_OTEL_ENABLED=1` plus `OTEL_EXPORTER_OTLP_ENDPOINT` for OTLP. See Observability.

---

## Serve Mode

> Run a single workflow as an HTTP server with Hono. Interact with it over REST, stream events via SSE, and manage approvals remotely.

Serve mode starts a Hono-based HTTP server alongside a running workflow. Every route operates on the single active run, with no workflow path or run ID in requests.

<Note>API reference: Server & Gateway lists every server and gateway export, its options, and links to source and tests.</Note>

## CLI

```bash
bunx smithers-orchestrator up workflow.tsx --serve --port 3000 --host 0.0.0.0 --auth-token "$SMITHERS_API_KEY"
```

A non-loopback `--host` (like `0.0.0.0`) requires `--auth-token` (or `--insecure` to opt out, which is dangerous). Binding to the default `127.0.0.1` needs neither.

| Flag | Default | Description |
|---|---|---|
| `--serve` | `false` | Enable HTTP server mode |
| `--port` | `7331` | TCP port |
| `--host` | `127.0.0.1` | Bind address |
| `--auth-token` | `SMITHERS_API_KEY` env | Bearer token for auth; required for a non-loopback `--host` |
| `--insecure` | `false` | Allow a non-loopback `--host` with no auth (exposes unauthenticated approve/deny/cancel; dangerous) |
| `--metrics` | `true` | Expose `/metrics` Prometheus endpoint |
| `--supervise` | `false` | Run the stale-run supervisor loop alongside `--serve` |

The process stays alive after the workflow completes so final state remains queryable. Ctrl+C stops both the server and the workflow.

Detached mode:

```bash
bunx smithers-orchestrator up workflow.tsx --serve --port 8080 -d
```

## Programmatic

```ts
import { SmithersDb, createServeApp } from "smithers-orchestrator";

const adapter = new SmithersDb(workflow.db);
const abort = new AbortController();

const app = createServeApp({
  workflow,
  adapter,
  runId,
  abort,
  authToken: "sk-secret",
});

Bun.serve({ port: 3000, fetch: app.fetch });
```

`createServeApp` returns a standard Hono app. Mount it with `Bun.serve`, pass it to another Hono app via `app.route()`, or use `app.fetch` in tests.

## ServeOptions

```ts
type ServeOptions = {
  workflow: SmithersWorkflow<unknown>;   // loaded workflow instance
  adapter: SmithersDb;               // Smithers DB adapter; e.g. new SmithersDb(workflow.db)
  runId: string;                     // active run ID
  abort: AbortController;            // shared cancellation controller
  authToken?: string;                // bearer token; falls back to SMITHERS_API_KEY; disabled if unset
  metrics?: boolean;                 // expose /metrics; default true
};
```

---

## Authentication

When `authToken` is configured, every route except `/health` requires:

- `Authorization: Bearer <token>`, or
- `x-smithers-key: <token>`

Missing or invalid tokens receive `401`.

---

## Routes

### GET /health

Returns `200` regardless of auth.

```json
{ "ok": true }
```

### GET /

Run status and node summary.

```json
{
  "runId": "run-1234",
  "workflowName": "bugfix",
  "status": "running",
  "startedAtMs": 1707500000000,
  "finishedAtMs": null,
  "summary": { "finished": 3, "in-progress": 1, "pending": 2 }
}
```

### GET /events

SSE stream of lifecycle events. Same format as the multi-workflow server.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `afterSeq` | `number` | `-1` | Only events after this sequence |

```
event: smithers
data: {"type":"NodeStarted","runId":"run-1234","nodeId":"analyze","iteration":0,"attempt":1}
id: 1

event: smithers
data: {"type":"NodeFinished","runId":"run-1234","nodeId":"analyze","iteration":0,"attempt":1}
id: 2
```

- Polls every 500ms.
- Auto-closes when the run reaches a terminal state.
- Reconnect with `?afterSeq=N` to resume from a known position.

### GET /frames

Rendered workflow frames.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | `number` | `50` | Max frames |
| `afterFrameNo` | `number` | - | Frames after this number |

### POST /approve/:nodeId

Approve a pending approval gate. All fields optional. Returns `{ "runId": "run-1234" }`.

```json
{
  "iteration": 0,
  "note": "Looks good",
  "decidedBy": "alice"
}
```

### POST /deny/:nodeId

Deny a pending approval gate. Same body as `/approve/:nodeId`.

### POST /cancel

Cancel the running workflow.

| Status | Code | Condition |
|---|---|---|
| 200 | - | Cancelled successfully |
| 409 | `RUN_NOT_ACTIVE` | Run is not actively running (e.g. already finished, failed, cancelled, continued, or its heartbeat has gone stale) |

> **Note:** Runs in `waiting-approval` or `waiting-timer` state are cancelled immediately and return `200`.

### GET /metrics

Prometheus text exposition. Same metrics as the multi-workflow server.

---

## Error Format

```json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description"
  }
}
```

Unknown routes return `404` with code `NOT_FOUND`.

---

## Serve Mode vs Multi-Workflow Server

| | Serve mode | Multi-workflow server |
|---|---|---|
| Scope | Single workflow, single run | Any workflow, multiple concurrent runs |
| Start | `bunx smithers-orchestrator up --serve` or `createServeApp()` | `startServer()` |
| Routes | `/`, `/events`, `/approve/:nodeId`, ... | `/v1/runs`, `/v1/runs/:runId`, ... |
| Framework | Hono | Node.js `http` |
| Use case | Development, single-purpose services | Production API gateway |

---

## Example

```bash
# Start a workflow with serve mode
bunx smithers-orchestrator up workflow.tsx --serve --port 8080 --auth-token sk-secret

# Status
curl http://localhost:8080/ -H "Authorization: Bearer sk-secret"

# Stream events
curl -N http://localhost:8080/events -H "Authorization: Bearer sk-secret"

# Approve
curl -X POST http://localhost:8080/approve/deploy \
  -H "Authorization: Bearer sk-secret" \
  -H "Content-Type: application/json" \
  -d '{"note": "Ship it", "decidedBy": "alice"}'

# Health (no auth)
curl http://localhost:8080/health
```

---

## Gateway

> WebSocket / RPC gateway for connecting clients and custom UIs to Smithers runs, providing pushed updates, subscriptions, metrics, and resilient reconnection.

`Gateway` is Smithers' headless control plane. Reach for it (instead of `startServer()`) when long-lived clients (bots, dashboards, schedulers, and custom UIs) need to authenticate once, stream events over WebSocket with resilient reconnection, decide approvals, inject signals, access metrics, and manage cron schedules across many registered workflows. Custom UIs, whether using the vanilla SDK or React hooks, rely on the Gateway to provide pushed updates and a stale-data-free model. For the single-workflow Hono-based HTTP surface, see Serve Mode (`createServeApp()` / `bunx smithers-orchestrator up --serve`).

<Warning>
  **Gateway-first client contract:** controllers, Bun cron jobs, monitors, bots,
  and custom clients must use this Gateway API. Do not open `smithers.db`,
  `.smithers/pg`, or Postgres directly; do not query `_smithers_*`; do not import
  `openSmithersStore` or CLI-internal `findAndOpenDb`; and do not pass
  `--backend` to read/control commands to hunt for runs. Direct backend access
  exists for runtime implementation, `smithers migrate`, backend integration
  tests, and maintainer diagnostics only. It is not a control-plane API.
</Warning>

<Note>API reference: Server & Gateway and Gateway Client list every gateway and client export, its options, and links to source and tests.</Note>

## Quick start

```tsx
import { Gateway, Task, Workflow, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  result: z.object({ ok: z.boolean() }),
});

const deploy = smithers((ctx) => (
  <Workflow name="deploy">
    <Task id="ship" output={outputs.result}>{{ ok: true }}</Task>
  </Workflow>
));

const gateway = new Gateway({
  heartbeatMs: 15_000,
  auth: {
    mode: "token",
    tokens: { "operator-token": { role: "operator", scopes: ["*"] } },
  },
});

gateway.register("deploy", deploy, { schedule: "0 8 * * 1-5" });
await gateway.listen({ port: 7331 });
```

```ts
const ws = new WebSocket("ws://localhost:7331");
ws.onmessage = (m) => console.log(JSON.parse(m.data));
ws.onopen = () => ws.send(JSON.stringify({
  type: "req",
  id: "c1",
  method: "connect",
  params: {
    minProtocol: 1,
    maxProtocol: 1,
    client: { id: "docs-example", version: "1.0.0", platform: "browser" },
    auth: { token: "operator-token" },
  },
}));
```

## Gateway client SDK

Programmatic clients (bots, schedulers, dashboards, third-party UIs) talk to the Gateway through the typed client SDK over the same RPC and WebSocket API. For the full custom-UI guide (declarative queries, pushed updates, stale guards, reconnect/resume, backpressure, optimistic mutations, auth, vanilla JS + React hooks) see Custom UIs.

```ts
import { SmithersGatewayClient } from "smithers-orchestrator/gateway-client";

const gateway = new SmithersGatewayClient();
const workflows = await gateway.listWorkflows();

// Resilient pushed updates: backoff + jitter on reconnect, resume from the last
// observed seq via run.gap_resync, stop on run.completed or abort.
for await (const frame of gateway.streamRunEventsResilient({ runId: "run-1" })) {
  if (frame.event === "run.completed") break;
}
```

Gateway client exports:

| Package | Exports |
|---|---|
| `smithers-orchestrator/gateway-client` | `SmithersGatewayClient`, `SmithersGatewayConnection`, `GatewayRpcError`, `gatewayBackoffDelay`, RPC frame/type-map types, extension envelope helpers/types, `GatewayUiBootConfig`, `SmithersGatewayClientOptions`, `createSmithersCollections`, `createSmithersDataClient`, `WorkspaceMode`, `SmithersCollections`, `SmithersDataClient`, collection row types, `gatewayKeys`, `smithersCollectionKeys`, `smithersLocalCollectionOptions`, `smithersElectricCollectionOptions`, `flattenGatewayRunNode`, `snapshotToGatewayRunNode`, `reconcileSnapshotNodes` |
| `smithers-orchestrator/gateway-react` | `SmithersGatewayProvider`, `SmithersCollectionsProvider`, `createGatewayReactRoot`, `useSmithersCollections`, `useGatewayRun`, `useGatewayRuns`, `useGatewayWorkflows`, `useGatewayApprovals`, `useGatewayNodeOutput`, `useGatewayRunEvents`, `useGatewayActions`, `useGatewayRpc`, `useSmithersGateway`, `useGatewayExtensionResource`, `useGatewayExtensionAction`, `useGatewayExtensionStream`, `useGatewayRunTree`, `useGatewayConnectionStatus`, `useGatewayCrons`, `useGatewayMemoryFacts`, `useGatewayScores`, `useGatewayTickets`, `useGatewayPrompts` |

## Bun controllers and cron jobs

Run one Gateway singleton per workspace. Discover its verified URL with the
public `gateway status` command rather than reading the runtime state file or
assuming port 7331 (the preferred port may already be occupied):

```ts
import { SmithersGatewayClient } from "smithers-orchestrator/gateway-client";

const statusProcess = Bun.spawnSync([
  "bunx", "smithers-orchestrator", "gateway", "status", "--format", "json",
], {
  cwd: process.cwd(),
  stdout: "pipe",
  stderr: "pipe",
});

if (!statusProcess.success) {
  throw new Error(new TextDecoder().decode(statusProcess.stderr));
}

const status = JSON.parse(new TextDecoder().decode(statusProcess.stdout));
if (!status.running || typeof status.url !== "string") {
  throw new Error("No healthy workspace Gateway is running");
}

const gateway = new SmithersGatewayClient({
  baseUrl: status.url,
  token: process.env.SMITHERS_API_KEY,
  client: { id: "local-health-cron", platform: "bun" },
});

const runs = await gateway.listRuns({ filter: { limit: 50 } });
```

Start `bunx smithers-orchestrator gateway` under your service manager when
`status.running` is false. A health cron should report the missing Gateway and
restart that singleton; it should not fall back to opening a store. Once it has
a run ID, use `getRun` for snapshots and `streamRunEventsResilient` for a live
cursor. Use `launchRun`, `resumeRun`, `cancelRun`, `submitApproval`, and
`submitSignal` for mutations so authentication, ownership, idempotency,
invalidation, and event emission stay intact.

<Note>
  **Bundling in Next.js / webpack:** in published versions up to and including
  0.28.0, `gateway-client` transitively imports
  `@smithers-orchestrator/electric-proxy`, which shipped raw TypeScript
  sources. Bun and Vite handle that natively, but webpack-based builds
  (Next.js) fail hard unless you add it to
  [`transpilePackages`](https://nextjs.org/docs/app/api-reference/config/next-config-js/transpilePackages):

  ```js
  // next.config.mjs
  export default {
    transpilePackages: ["@smithers-orchestrator/electric-proxy"],
  };
  ```

  Releases after 0.28.0 ship the package as plain JavaScript with bundled type
  declarations, so the workaround is not needed there.
</Note>

## REST domain API

The Gateway also exposes a typed REST domain surface under `/v1/api/*`. These
routes share the same auth modes, scope checks, validation, and backend code
paths as their `/v1/rpc/<method>` twins. Responses use `{ ok: true, data }` for
reads and `{ ok: false, error }` for failures.

Mutating routes include a write acknowledgement. SQLite and embedded PGlite
return `{ seq }`, where `seq` is the invalidation frame emitted by
`GET /v1/api/stream`. Real Postgres returns `{ txid }`, a numeric string from
`pg_current_xact_id()::xid::text`, for Electric transaction matching.

```toon
api[24]{route,rpc,scope}:
  GET /v1/api/runs,listRuns,run:read
  GET /v1/api/runs/:id,getRun,run:read
  POST /v1/api/runs,launchRun,run:write
  POST /v1/api/runs/:id/resume,resumeRun,run:write
  POST /v1/api/runs/:id/cancel,cancelRun,run:write
  POST /v1/api/runs/:id/rewind,rewindRun,run:admin
  GET /v1/api/runs/:id/tree,getDevToolsSnapshot,observability:read
  GET /v1/api/events,streamRunEvents tail snapshot,run:read
  GET /v1/api/nodes/:runId/:nodeId/output,getNodeOutput,run:read
  GET /v1/api/nodes/:runId/:nodeId/diff,getNodeDiff,run:read
  GET /v1/api/approvals,listApprovals,run:read
  POST /v1/api/approvals/:id,submitApproval,approval:submit
  POST /v1/api/signals,submitSignal,signal:submit
  GET /v1/api/workflows,listWorkflows,run:read
  GET /v1/api/docs,listDocs,run:read
  GET /v1/api/prompts,listPrompts,prompt:read
  GET /v1/api/scores,listScores,score:read
  GET /v1/api/tickets,listTickets,ticket:read
  GET /v1/api/memory-facts,listMemoryFacts,memory:read
  GET /v1/api/crons,cronList,cron:read
  GET /v1/api/accounts,listAccounts,account:read
  GET /v1/api/schema-signature,getSchemaSignature,run:read
  GET /v1/api/stream,SSE invalidation feed,run:read
```

`GET /v1/api/stream` is a `text/event-stream` invalidation feed for local
TanStack Query collections. Change events are `event: change` frames with
`data: {"seq": number, "collections": string[]}`. Writes that happen inside a
short coalescing window emit one frame with the union of collection names.

Clients should send `Last-Event-ID` when reconnecting. The Gateway replays
missed frames from its bounded ring buffer when possible. If the requested seq
is too old, it sends `event: reset` with the current seq so the client can
invalidate all collection query keys. Each connection also has a bounded
outbound queue; a slow consumer is resynced through the same reset path, which
bounds memory per stream.

## RPC methods (TOON)

```toon
rpc[27]{method,params,returns,scope,transport}:
  launchRun,workflow/input?/options.runId?/options.idempotencyKey?,{runId/workflow},run:write,http+websocket
  resumeRun,runId/options.force?,{runId/status},run:write,http+websocket
  cancelRun,runId,{runId/status:cancelling},run:write,http+websocket
  hijackRun,runId/options?,{runId/status:hijack-ready/sessionId},run:admin,http+websocket
  rewindRun,runId/frameNo/confirm:true,JumpResult,run:admin,http+websocket
  submitApproval,runId/nodeId/iteration?/decision,{runId/nodeId/iteration/approved},approval:submit,http+websocket
  submitSignal,runId/correlationKey/payload?/signalName?,Delivery metadata,signal:submit,http+websocket
  getRun,runId,Run record + optional runState,run:read,http+websocket
  listRuns,filter.status?/filter.limit?/filter.workflow?,Run summaries,run:read,http+websocket
  listWorkflows,filter.hasUi?,Workflow summaries,run:read,http+websocket
  listApprovals,filter.runId?/filter.workflow?/filter.limit?,Pending approvals,run:read,http+websocket
  streamRunEvents,runId/afterSeq?,{streamId/runId/afterSeq/currentSeq},run:read,websocket
  streamDevTools,runId/afterSeq?/fromSeq?,{streamId/runId/fromSeq/afterSeq} + devtools.event frames,observability:read,websocket
  getDevToolsSnapshot,runId/frameNo?,DevTools snapshot payload,observability:read,http+websocket
  getNodeOutput,runId/nodeId/iteration?,NodeOutputResponse,run:read,http+websocket
  getNodeDiff,runId/nodeId/iteration?,Node diff response,run:read,http+websocket
  cronList,filter.workflow?,Cron rows,cron:read,http+websocket
  cronCreate,workflow/pattern/cronId?/enabled?,Created cron row,cron:write,http+websocket
  cronDelete,cronId,{cronId/removed},cron:write,http+websocket
  cronRun,cronId? or workflow/input?,{runId/workflow},cron:write,http+websocket
  listAccounts,,Registered agent accounts (api keys redacted),account:read,http+websocket
  listMemoryFacts,namespace?,Memory facts,memory:read,http+websocket
  listScores,runId/nodeId?,Scorer results,score:read,http+websocket
  listTickets,kind?,Work docs,ticket:read,http+websocket
  createTicket,path/content/kind?/status?,Created doc row,ticket:write,http+websocket
  updateTicket,path/content?/status?,Updated doc row,ticket:write,http+websocket
  deleteTicket,path,{path/deleted},ticket:write,http+websocket
```

`health` remains available as a utility RPC and `GET /health` is available without auth. The legacy method names are still accepted for compatibility (`runs.create`, `runs.get`, `runs.list`, `runs.cancel`, `runs.rerun`, `runs.diff`, `frames.list`, `frames.get`, `attempts.list`, `attempts.get`, `workflows.list`, `approvals.list`, `approvals.decide`, `signals.send`, `cron.list`, `cron.add`, `cron.remove`, `cron.trigger`, `jumpToFrame`, `devtools.jumpToFrame`, `devtools.getNodeOutput`, `devtools.getNodeDiff`), but new clients should use the v1 names above.

### Scopes

```toon
scopes[14]{scope,allows}:
  run:read,Read run state/lists/event streams/node output/node diffs
  run:write,Launch/resume/cancel runs; implies run:read
  run:admin,Hijack or rewind runs; implies run:write and run:read
  approval:submit,Submit approval decisions
  signal:submit,Submit workflow signals
  cron:read,List cron schedules
  cron:write,Create/delete/trigger cron schedules; implies cron:read
  account:read,List registered agent accounts (api keys redacted)
  memory:read,List cross-run memory facts
  prompt:read,List workspace prompts
  score:read,List scorer/eval results for a run
  ticket:read,List work docs (tickets/plans/specs/proposals)
  ticket:write,Create/update/soft-delete work docs; implies ticket:read
  observability:read,Read DevTools and other observability streams
```

`*` grants every scope. Pass a method name string in the `scopes` array (e.g. `"launchRun"`) to grant access to exactly that RPC call. Legacy wildcard method grants such as `cron.*` continue to match legacy method names; typed scopes are the contract to use for new integrations. Legacy ranked grants (`read`, `execute`, `approve`, `admin`) are accepted so older tokens keep working.

### `rewindRun` (destructive rewind)

Rewinds a run to a prior frame and makes it resumable from that point.
This is destructive: it truncates frames, attempts, output rows, and
diff-cache entries beyond the target; reverts JJ sandboxes; marks the
run `running` again; and emits a `TimeTravelJumped` event so
`streamDevTools` subscribers rebaseline.

Caller identity is authorized per-request: the connection must have
`run:admin` scope and must also be the run owner (`userId` matches
`ownerId`) or have `role: "admin"`. Scope alone never grants access.
The legacy aliases `jumpToFrame` and `devtools.jumpToFrame` route to
`rewindRun`.

Request:

```ts
type RewindRunRequest = {
  runId: string;     // /^[a-z0-9_-]{1,64}$/
  frameNo: number;   // 0 <= frameNo <= latestFrameNo
  confirm: true;     // must be literal true
};
```

Response (`JumpResult`):

```ts
type JumpResult = {
  ok: true;
  newFrameNo: number;
  revertedSandboxes: number;
  deletedFrames: number;
  deletedAttempts: number;
  invalidatedDiffs: number;
  durationMs: number;
};
```

Also broadcast after the DB commit as `run.time_travel_jumped` with
`{ runId, fromFrameNo, toFrameNo, timestampMs, caller }`.

Quota: 10 rewinds per run per caller per hour (default window). Exceeded
→ `RateLimited`.

Failure modes and HTTP status:

| Code                   | Meaning                                                                   | HTTP  |
| ---------------------- | ------------------------------------------------------------------------- | ----- |
| `InvalidRunId`         | `runId` fails `/^[a-z0-9_-]{1,64}$/`.                                     | `400` |
| `InvalidFrameNo`       | `frameNo` is not a non-negative i32 integer.                              | `400` |
| `ConfirmationRequired` | Caller omitted `confirm: true`.                                           | `400` |
| `FrameOutOfRange`      | `frameNo` > latest frame, or run has no frames.                           | `400` |
| `Unauthorized`         | Caller is neither the run owner nor an admin (audit row still written).   | `401` |
| `RunNotFound`          | `runId` does not exist.                                                   | `404` |
| `Busy`                 | Another rewind is in flight for this run.                                 | `409` |
| `RateLimited`          | Caller exceeded rewind quota (default 10/hour).                           | `429` |
| `UnsupportedSandbox`   | A sandbox cannot be reverted (missing / untrackable `jjPointer`).         | `501` |
| `VcsError`             | A JJ revert call failed; DB/reconciler rolled back.                       | `500` |
| `RewindFailed`         | Rewind failed and rollback was partial; run marked `needs_attention`.     | `500` |

Every call, whether success, failure, or unauthorized, writes one row to
`_smithers_time_travel_audit` with `result ∈ { success, failed, partial, in_progress }`.
An in-progress row is inserted before any mutation and updated in place
on completion; startup recovery flips any leftover `in_progress` rows to
`partial`.

### Node output

`getNodeOutput` returns the DevTools Output-tab payload for a single task iteration:

```ts
type NodeOutputResponse = {
  status: "produced" | "pending" | "failed";
  row: Record<string, unknown> | null;
  schema: OutputSchemaDescriptor | null;
  partial?: Record<string, unknown> | null; // only when status === "failed"
};

type OutputSchemaDescriptor = {
  fields: Array<{
    name: string;
    type: "string" | "number" | "boolean" | "object" | "array" | "null" | "unknown";
    optional: boolean;
    nullable: boolean;
    description?: string;
    enum?: readonly unknown[];
  }>;
};
```

### Error codes

Gateway v1 RPC errors use stable code strings and HTTP status mappings:

```toon
errors[22]{code,http}:
  InvalidRequest,400
  InvalidInput,400
  Unauthorized,401
  Forbidden,403
  RunNotFound,404
  RUN_NOT_ACTIVE,409
  CronNotFound,404
  TicketNotFound,404
  NodeNotFound,404
  IterationNotFound,404
  NodeHasNoOutput,404
  FrameOutOfRange,400
  SeqOutOfRange,400
  Busy,409
  AlreadyDecided,409
  RateLimited,429
  PayloadTooLarge,413
  BackpressureDisconnect,429
  UnsupportedSandbox,501
  VcsError,500
  RewindFailed,500
  Internal,500
```

The table above is the canonical v1 registry used by the SDK and OpenAPI.
Current server responses can also surface legacy aliases from older Gateway
paths. Treat these aliases by their meaning and HTTP status; new clients should
prefer canonical codes where returned and tolerate aliases on older paths:

```toon
legacyErrors[12]{code,meaning,http}:
  INVALID_REQUEST,Invalid request,400
  INVALID_INPUT,Invalid input,400
  INVALID_FRAME,Invalid frame,400
  PROTOCOL_UNSUPPORTED,Unsupported protocol,400
  UNAUTHORIZED,Unauthorized,401
  FORBIDDEN,Forbidden,403
  NOT_FOUND,Not found,404
  METHOD_NOT_FOUND,Unknown method,404
  PAYLOAD_TOO_LARGE,Payload too large,413
  InvalidRunId,Invalid run id,400
  InvalidFrameNo,Invalid frame number,400
  ConfirmationRequired,Confirmation required,400
```

### Versioned wire shapes

All DevTools wire types carry `version: 1`.

`DevToolsSnapshot` (v1):

```ts
type DevToolsSnapshot = {
  version: 1;
  runId: string;
  frameNo: number;   // latest frame reflected in this tree
  seq: number;       // monotonic sequence id (equals frameNo today)
  root: DevToolsNode;
};

type DevToolsNode = {
  id: number;        // stable across frames for the same logical node
  type: "workflow" | "task" | "sequence" | "parallel" | /* …see protocol */;
  name: string;
  props: Record<string, unknown>;
  task?: { nodeId: string; kind: "agent" | "compute" | "static"; /* … */ };
  children: DevToolsNode[];
  depth: number;
};
```

`DevToolsDelta` (v1):

```ts
type DevToolsDelta = {
  version: 1;
  baseSeq: number;   // must match the subscriber's current seq
  seq: number;       // new seq after applying ops, in order
  ops: Array<
    | { op: "addNode"; parentId: number; index: number; node: DevToolsNode }
    | { op: "removeNode"; id: number }
    | { op: "updateProps"; id: number; props: Record<string, unknown> }
    | { op: "updateTask"; id: number; task: DevToolsNode["task"] }
    | { op: "replaceRoot"; node: DevToolsNode } // emitted when the root's
                                                // identity or shape changes;
                                                // `removeNode` of the root is
                                                // never emitted.
  >;
};
```

`DevToolsEvent` (v1), frames pushed over `devtools.event`:

```ts
type DevToolsEvent =
  | { version: 1; kind: "snapshot"; snapshot: DevToolsSnapshot }
  | { version: 1; kind: "delta"; delta: DevToolsDelta };
```

A subscription always starts with a `snapshot` event, then emits `delta` events
per frame. The server re-baselines (emits a full `snapshot` instead of a
`delta`) after 50 delta events, when a delta is larger than a fresh snapshot,
or when the gateway observes `TimeTravelJumped` for the run.

## WebSocket protocol

Three frame types share the same socket:

- `req`: `{ type: "req", id, method, params? }` from client.
- `res`: `{ type: "res", id, ok, payload?, error? }` from server, correlated by `id`.
- `event`: `{ type: "event", event, payload?, seq, stateVersion }` server-pushed; `seq` is per connection, `stateVersion` is global.

Handshake: on connect the server immediately pushes `connect.challenge` (`{ nonce, ts }`). The client replies with a `connect` request carrying `minProtocol`, `maxProtocol`, `client` metadata, `auth`, and an optional `subscribe: string[]` to filter events by `runId`. The server returns a `hello` payload (`protocol`, `features`, `policy.heartbeatMs`, `auth` with `sessionToken`/`role`/`scopes`/`userId`, `snapshot`).

After `connect`, the gateway emits `tick` events every `heartbeatMs`. `launchRun`, `submitApproval`, `submitSignal`, and `cronRun` automatically subscribe the connection to the affected `runId`. Server-pushed event names:

| Event | Category |
|---|---|
| `connect.challenge` | Connection |
| `tick` | Connection |
| `run.event` | Run lifecycle |
| `run.heartbeat` | Run lifecycle |
| `run.gap_resync` | Run lifecycle |
| `run.error` | Run lifecycle |
| `run.completed` | Run lifecycle |
| `run.time_travel_jumped` | Run lifecycle |
| `node.started` | Run lifecycle |
| `node.finished` | Run lifecycle |
| `node.failed` | Run lifecycle |
| `task.output` | Run lifecycle |
| `task.heartbeat` | Run lifecycle |
| `approval.requested` | Approval |
| `approval.decided` | Approval |
| `approval.auto_approved` | Approval |
| `cron.triggered` | Cron |
| `devtools.event` | DevTools |

For stateless callers, `POST /rpc` accepts the same body shape (`{ id, method, params }`) and returns the same `ResponseFrame`. Auth headers: `Authorization: Bearer <token>` or `x-smithers-key: <token>` (or trusted-proxy headers in trusted-proxy mode).

## GatewayOptions

```ts
type GatewayOptions = {
  protocol?: number;                 // default 1
  features?: string[];               // default ["streaming", "runs"]
  heartbeatMs?: number;              // default 15_000
  auth?: GatewayAuthConfig;
  workspaceRoot?: string;            // anchors disk-backed reads and the advertised identity
  identity?: {                       // advertised on GET /health, the health RPC, and the WS hello
    backend?: string;                // sqlite | pglite | postgres
    version?: string;                // serving package version
  };
  ui?: GatewayUiConfig;              // custom gateway UI; true mounts the built-in console
  operatorUi?: GatewayOperatorUiConfig | false; // default { path: "/console" }; false disables
  defaults?: {
    cliAgentTools?: "all" | "explicit-only";
    outOfProcessEventBridge?: boolean;
    outOfProcessEventBridgePollMs?: number;
  };
  maxBodyBytes?: number;             // default 1_048_576 for POST /rpc
  maxPayload?: number;               // default 1_048_576 for WebSocket frames
  maxConnections?: number;           // default 1_000
  eventWindowSize?: number;          // default 10_000 per-run replay frames
  outOfProcessEventBridge?: boolean; // default true; streams persisted events from detached runs
  outOfProcessEventBridgePollMs?: number; // default 1_000
  headersTimeout?: number;           // default 30_000
  requestTimeout?: number;           // default 60_000
};

type GatewayOperatorUiConfig = {
  path?: string;                      // default "/console"
  title?: string;
  props?: Record<string, unknown>;
};

type GatewayUiConfig =
  | true
  | {
      entry: string;
      path?: string;                  // gateway default "/"; workflow default "/workflows/<workflowKey>"
      title?: string;
      props?: Record<string, unknown>;
    };

type GatewayTokenGrant = {
  role: string;
  scopes: string[];
  userId?: string;
  tokenId?: string;
  issuedAtMs?: number;
  expiresAtMs?: number;
  revokedAtMs?: number;
};

type GatewayAuthConfig =
  | {
      mode: "token";
      tokens: Record<string, GatewayTokenGrant>;
      allowedOrigins?: string[];     // default [] (no Origin allowlist)
    }
  | {
      mode: "jwt";
      issuer: string;
      audience: string | string[];
      secret: string;                // HS256
      scopesClaim?: string;          // default "scope"
      roleClaim?: string;            // default "role"
      userClaim?: string;            // default "sub"
      defaultRole?: string;          // default "operator"
      defaultScopes?: string[];      // default [] when scope claim is absent
      clockSkewSeconds?: number;     // default 60; negative values clamp to 0
      allowedOrigins?: string[];     // default [] (no Origin allowlist)
    }
  | {
      mode: "trusted-proxy";
      allowedOrigins?: string[];     // default [] (no Origin allowlist)
      trustedHeaders?: string[];     // default ["x-user-id","x-user-scopes","x-user-role"]
      defaultRole?: string;          // default "operator"
      defaultScopes?: string[];      // trusted-proxy: used when the scopes header is absent, else the request is rejected
    };
```

JWT auth reads scopes from `scope`, role from `role`, and user id from `sub` unless the `*Claim` options override those claim names. Missing JWT role falls back to `defaultRole` and then `operator`; missing JWT scopes fall back to `defaultScopes` and then `[]`. Trusted-proxy auth reads `trustedHeaders` as `[user, scopes, role]`; missing role falls back to `defaultRole` and then `operator`, and missing scopes fall back to `defaultScopes`, or the request is rejected when no `defaultScopes` is configured.

`allowedOrigins` is available in every mode (token, jwt, trusted-proxy) as defense-in-depth. It defaults to `[]`, which enforces no Origin allowlist. When non-empty, the gateway rejects any HTTP RPC or WebSocket upgrade whose browser `Origin` header is not on the list; requests with no `Origin` header (server-to-server / CLI callers) are always allowed. Set it to your operator-UI origin(s) when exposing a token/jwt gateway to a browser.

Runs started through the gateway expose `ctx.auth = { triggeredBy, role, scopes, createdAt }`. `<Approval>` may further restrict decisions with `allowedScopes` and `allowedUsers`, which the gateway enforces before accepting `submitApproval`.

`headersTimeout` and `requestTimeout` are applied to the underlying Node HTTP server when `gateway.listen()` starts. Keep both below the corresponding reverse-proxy idle/read timeouts so slow clients are closed by Smithers first.

## Notes

- Identity: `GET /health`, the `health` RPC, and the WS hello carry `identity: { workspaceRoot, backend, version, pid, startedAtMs }`. Clients (the CLI's `ui`/`gateway status` among them) verify `workspaceRoot` against the workspace they resolved locally instead of trusting whichever process owns the port.
- Bind failures reject: `gateway.listen()` rejects on `EADDRINUSE` (and any other bind error) instead of crashing the process. The `smithers gateway` command retries on an ephemeral port and records the real port in its runtime state file.
- Singleton per workspace: `smithers gateway` writes `{pid, host, port, url, token, workspaceRoot, backend, version, protocol, startedAtMs}` to `<tmpdir>/smithers-gateway/<workspace-hash>.json` (mode 0600) after listening, refuses to start when a healthy gateway for the same workspace is already running, and cleans the file up on shutdown. `smithers gateway status` and `smithers gateway stop` manage it; stale files (dead pid, identity mismatch) are cleaned up on the next discovery.
- Monitor mount: the `smithers gateway` CLI singleton also serves the Smithers Monitor, a live web UI over every run in the workspace, at `/monitor` (open it with `bunx smithers-orchestrator monitor`). The mount ships inside the CLI, so it needs no `.smithers/` pack and no registered workflow UI. Library embedders control the other UI mounts through `ui` (custom gateway UI, workflow UIs at `/workflows/<key>`) and `operatorUi` (the embeddable operator console, default `/console`).
- `--mint-token`: mints a random bearer, requires it on every request, and records it only in the runtime state file (plus the daemon's own stderr). `bunx smithers-orchestrator ui` reads the token from the state file automatically for CLI RPC calls, but browser navigation to the printed workflow UI URL cannot send that bearer header until UI token injection ships; use `smithers gateway` without `--mint-token` when you need browser-served workflow UIs. Other clients can read `SMITHERS_TOKEN` / `SMITHERS_API_KEY`.
- Cron: `gateway.register(name, wf, { schedule })` writes a cron row keyed `gateway:<name>`; the gateway polls between 1 s and 15 s (clamped from `heartbeatMs`). Cron-fired runs get `ctx.auth.role = "system"`, `triggeredBy = "cron:gateway"`, `scopes = ["*"]`.
- Host defense: an unauthenticated gateway (the autostart default) rejects any request whose `Host` header is non-loopback, as a DNS-rebinding defense, so binding `--host 0.0.0.0` without a token returns 403 `Host is not allowed`. This applies only when no `auth` is configured. To deliberately expose an unauthenticated remote bind, pass `smithers gateway --insecure` (or `up --serve --insecure`), which trusts any Host; `SMITHERS_GATEWAY_TRUST_ANY_HOST=1` (gateway) and `SMITHERS_SERVE_TRUST_ANY_HOST=1` (serve) do the same via the environment.
- JWT mode currently validates `alg=HS256`, HMAC, `iss`, `aud`, `exp`, `nbf`. Scope claims may be arrays or space/comma-separated strings.
- Trusted-proxy mode is only safe behind something you control (Cloudflare Access, internal API gateway) that strips and rewrites identity headers.
- DevTools streams: see Versioned wire shapes for re-baseline triggers; over-capacity subscribers receive `BackpressureDisconnect`.

---

## MCP Server

> Expose Smithers as a Model Context Protocol stdio server so any MCP client (Claude Code, Cursor, Codex, or your own agent) can list, run, inspect, and control workflows without shell scripting.

Smithers ships a built-in MCP stdio server. Passing `--mcp` to the CLI speaks the Model Context Protocol over stdin/stdout instead of acting as an interactive CLI. Any MCP-aware client can connect, discover workflows, start runs, watch progress, resolve approvals, and revert bad attempts through structured tool calls.

Use the MCP server when an AI agent should drive Smithers autonomously. Use the HTTP Server for REST endpoints for human-written code or webhooks.

---

## Setup

### Start the server

```bash
bunx smithers-orchestrator --mcp
```

This starts the semantic surface: a stable, structured tool set for AI agent consumption, documented on this page.

Two additional surfaces are available via `--surface`:

```bash
# Semantic tools only (default)
bunx smithers-orchestrator --mcp --surface semantic

# Raw CLI-mirroring tools only
bunx smithers-orchestrator --mcp --surface raw

# Both surfaces registered on the same server
bunx smithers-orchestrator --mcp --surface both
```

Use `--surface raw` only for direct CLI parity. Prefer the semantic surface for new integrations: every tool returns a `{ ok, data, error }` envelope with Zod-validated input and output schemas.

Scope the semantic surface when a client should not receive every Smithers control tool:

```bash
# Expose only selected semantic tools
bunx smithers-orchestrator --mcp --allowed-tools list_workflows,get_run

# Expose only tools annotated as read-only
bunx smithers-orchestrator --mcp --read-only
```

`--allowed-tools` accepts a comma-separated list of semantic tool names. Passing an empty allowlist intentionally exposes no semantic tools. `--read-only` removes semantic tools with write or control side effects, such as starting runs, resolving approvals, or reverting attempts. With `--surface both`, these controls apply to the semantic toolset only; raw CLI-mirroring tools are still registered by the raw surface.

### Register manually

For clients that read JSON config directly:

```json
{
  "mcpServers": {
    "smithers": {
      "command": "bunx",
      "args": ["smithers-orchestrator", "--mcp"]
    }
  }
}
```

Project-scoped install (e.g. a monorepo where Smithers is a dev dependency; ensure `smithers-orchestrator` is in the local `package.json`):

```json
{
  "mcpServers": {
    "smithers": {
      "command": "bunx",
      "args": ["smithers-orchestrator", "--mcp"]
    }
  }
}
```

### If `mcp add` fails

`bunx smithers-orchestrator mcp add` hands the launch command to a registration helper that expects it as a single argument. If a runner or shell word-splits it, the helper sees a bare `--mcp` token and aborts:

```
Registering MCP server...
code: MCP_ADD_FAILED
message: "error: unknown option '--mcp'"
```

Register with your agent's own CLI instead. The `--` separator tells the agent that everything after it is the launch command, so it never parses `--mcp` as one of its own flags:

```bash
codex mcp add smithers -- bunx smithers-orchestrator --mcp
claude mcp add smithers -- bunx smithers-orchestrator --mcp
```

Any MCP-aware CLI follows the same `<agent> mcp add <name> -- <command>` shape. Or write the JSON or TOML config by hand using the snippets above. The Smithers CLI prints these fallback commands automatically whenever `mcp add` fails.

---

## Tool Registration

On start, each tool is registered with its input schema, output schema, and MCP annotations. Every tool carries:

- **`inputSchema`**: Zod object describing accepted parameters.
- **`outputSchema`**: Zod schema for the structured response envelope.
- **`annotations`**: MCP annotation metadata (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`).

### Structured tool envelope

Every tool returns the same shape:

```ts
{
  ok: boolean;
  data?: { ... };     // present on success
  error?: {           // present on failure
    code: string;
    message: string;
    details?: Record<string, unknown> | null;
    docsUrl?: string | null;
  };
}
```

The response is also echoed as a `text` content block, so clients that do not parse `structuredContent` still receive the JSON payload.

### Tool annotations

| Annotation | Tools | Meaning |
|---|---|---|
| `readOnlyHint: true` | Most query tools | Tool does not modify state |
| `readOnlyHint: false, openWorldHint: true` | `run_workflow` | Launches external processes |
| `readOnlyHint: false, destructiveHint: true, idempotentHint: false` | `resolve_approval`, `revert_attempt`, `rewind_run`, `restore_checkpoint`, `time_travel` | Mutates persisted state irreversibly |
| `readOnlyHint: false, idempotentHint: false` | `fork_run`, `replay_run` | Creates new run/branch state |

---

## Tool Reference

### list_workflows

List all Smithers workflows discovered in the working directory.

**Input:** none

**Output:**

```ts
{
  workflows: Array<{
    id: string;
    metadataVersion: number;
    displayName: string;
    scope: "local" | "global";
    entryFile: string;
    path: string;
    sourceType: string;
    description: string;
    tags: string[];
    aliases: string[];
  }>;
}
```

Use the returned `id` values as the `workflowId` parameter for `run_workflow`.

---

### run_workflow

Start or resume a discovered workflow.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `workflowId` | `string` | required | Workflow ID from `list_workflows` |
| `input` | `Record<string, unknown>` | `{}` | Workflow input object |
| `prompt` | `string` | - | Shorthand: sets `input.prompt` when `input` is not provided |
| `runId` | `string` | auto | Custom run ID |
| `resume` | `boolean` | `false` | Resume an existing run; requires `runId` |
| `force` | `boolean` | `false` | Force-start even if a run with this ID already exists |
| `waitForTerminal` | `boolean` | `false` | Block until the run reaches a terminal state |
| `waitForStartMs` | `number` | `1000` | For background launches, how long to wait for the run row to appear in the database |
| `maxConcurrency` | `number` | - | Max concurrent nodes |
| `rootDir` | `string` | - | Root directory for tool sandboxing and path resolution |
| `logDir` | `string` | - | Directory for log files |
| `allowNetwork` | `boolean` | `false` | Allow network access in `bash` tool |
| `maxOutputBytes` | `number` | - | Cap on node output size |
| `toolTimeoutMs` | `number` | - | Per-tool call timeout |
| `hot` | `boolean` | `false` | Enable hot-reloading of the workflow file |

**Output:**

```ts
{
  workflow: {
    id: string;
    metadataVersion: number;
    displayName: string;
    scope: "local" | "global";
    entryFile: string;
    path: string;
    sourceType: string;
    description: string;
    tags: string[];
    aliases: string[];
  };
  runId: string;
  launchMode: "background" | "waited";
  requestedResume: boolean;
  status: string;
  observedRun: RunSummary | null;
  result: { runId, status, output?, error? } | null;
}
```

**Background vs. waited launch**

By default (`waitForTerminal: false`) the tool fires the workflow and returns immediately with `launchMode: "background"`. `observedRun` reflects the run state polled during `waitForStartMs`. Use `watch_run` to track progress.

Set `waitForTerminal: true` to block until the workflow finishes. `result` is populated and `launchMode` is `"waited"`.

**Run option forwarding**

`rootDir`, `logDir`, `allowNetwork`, `maxOutputBytes`, `toolTimeoutMs`, and `hot` are forwarded verbatim to `runWorkflow`. They override values baked into the workflow file.

---

### list_runs

List recent runs with summary data.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | `number` (1–200) | `20` | Max runs to return |
| `status` | `string` | - | Filter by status (`running`, `finished`, `failed`, etc.) |

**Output:**

```ts
{
  runs: RunSummary[];
}
```

`RunSummary` fields: `runId`, `workflowName`, `workflowPath`, `parentRunId`, `status`, `createdAtMs`, `startedAtMs`, `finishedAtMs`, `heartbeatAtMs`, `activeNodeId`, `activeNodeLabel`, `pendingApprovalCount`, `waitingTimers`, `countsByState`.

---

### get_run

Get the full detail record for a specific run, including steps, approvals, timers, loop state, lineage, config, and error.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `runId` | `string` | Run ID |

**Output:**

```ts
{
  run: RunSummary & {
    steps: Array<{ nodeId, iteration, state, lastAttempt, updatedAtMs, outputTable, label }>;
    approvals: PendingApproval[];
    loops: Array<{ loopId, iteration, maxIterations }>;
    continuedFromRunIds: string[];
    activeDescendantRunId: string | null;
    config: unknown | null;
    error: unknown | null;
  };
}
```

---

### watch_run

Poll a run at a fixed interval until it reaches a terminal state or a timeout expires.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run to watch |
| `intervalMs` | `number` | `1000` | Poll interval (minimum enforced by runtime) |
| `timeoutMs` | `number` | `30000` | Wall-clock budget before giving up |

**Output:**

```ts
{
  runId: string;
  intervalMs: number;
  pollCount: number;
  reachedTerminal: boolean;
  timedOut: boolean;
  finalRun: RunSummary;
  snapshots: Array<{ observedAtMs: number; run: RunSummary }>;
}
```

When `timedOut` is `true` the run is still active, so call `watch_run` again or raise `timeoutMs`. Terminal statuses: any status other than `running`, `waiting-approval`, `waiting-event`, or `waiting-timer`, including `finished`, `failed`, `cancelled`, and `continued`.

---

### explain_run

Return a structured diagnosis explaining why a run is blocked, waiting, or stale.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `runId` | `string` | Run ID |

**Output:**

```ts
{
  diagnosis: {
    runId: string;
    status: string;
    summary: string;
    generatedAtMs: number;
    blockers: Array<{
      kind: string;
      nodeId: string;
      iteration: number | null;
      reason: string;
      waitingSince: number;
      unblocker: string;
      context?: string;
      signalName?: string | null;
      dependencyNodeId?: string | null;
      firesAtMs?: number | null;
      remainingMs?: number | null;
      attempt?: number | null;
      maxAttempts?: number | null;
    }>;
    currentNodeId: string | null;
  };
}
```

`summary` is a human-readable sentence. `blockers` lists every node preventing progress; `unblocker` describes what action or event would unblock it.

---

### list_pending_approvals

List approvals that are waiting for a human decision, optionally filtered by run, workflow, or node.

**Input:** All parameters optional. Omit all to list every pending approval across all runs.

| Parameter | Type | Description |
|---|---|---|
| `runId` | `string` | Filter by run ID |
| `workflowName` | `string` | Filter by workflow name |
| `nodeId` | `string` | Filter by node ID |

**Output:**

```ts
{
  approvals: Array<{
    runId: string;
    nodeId: string;
    iteration: number;
    status: string;
    requestedAtMs: number | null;
    decidedAtMs: number | null;
    note: string | null;
    decidedBy: string | null;
    request: unknown;
    decision: unknown;
    autoApproved?: boolean;
    workflowName: string | null;
    runStatus: string | null;
    nodeLabel: string | null;
  }>;
}
```

---

### resolve_approval

Approve or deny a pending approval. This tool is destructive and non-idempotent.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `action` | `"approve" \| "deny"` | required, decision to record |
| `runId` | `string` | Filter to a specific run |
| `workflowName` | `string` | Filter by workflow name |
| `nodeId` | `string` | Filter by node ID |
| `iteration` | `number` | Filter by loop iteration |
| `note` | `string` | Optional note to record with the decision |
| `decidedBy` | `string` | Identity of the decision-maker |
| `decision` | `unknown` | Structured decision payload passed back to the workflow |

**Ambiguity guard**

Zero matches errors with `INVALID_INPUT`. More than one match errors with `INVALID_INPUT` and returns matches in `details.matches`; add `runId`, `nodeId`, or `iteration` to narrow the selection. The tool never guesses when multiple approvals match.

**Output:**

```ts
{
  action: "approve" | "deny";
  approval: PendingApproval;   // with updated status, decidedAtMs, note, decidedBy
  run: RunSummary | null;
}
```

---

### ask_human

Block the current run and ask a human to make a decision, then wait for their answer. Use this whenever the agent is blocked, uncertain, missing information, or about to take an irreversible or destructive action, instead of guessing. The tool creates a durable, pending human request and returns only once it is resolved.

When run inside a Smithers task, the run/node context is taken from the `SMITHERS_RUN_ID` / `SMITHERS_NODE_ID` / `SMITHERS_ITERATION` environment variables Smithers injects into the agent; pass `runId`/`nodeId`/`iteration` explicitly to override, or rely on single-active-run autodetection.

The orchestrating agent resolves the request on the human's behalf: relay the question to the human in conversation, collect their decision, then run `bunx smithers-orchestrator human answer <requestId> --value '<json>'` (or `bunx smithers-orchestrator human cancel <requestId>`) yourself; never instruct the human to run these. `bunx smithers-orchestrator human inbox` lists everything waiting.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `prompt` | `string` | required, the decision or question to put to a human |
| `context` | `string` | Extra context appended to the prompt |
| `choices` | `string[]` | Fixed choices; restricts the human's answer to one of these |
| `runId` | `string` | Run to attach to (default: `SMITHERS_RUN_ID` or the single active run) |
| `nodeId` | `string` | Node to attach to (default: `SMITHERS_NODE_ID`) |
| `iteration` | `number` | Loop iteration (default: `SMITHERS_ITERATION` or 0) |
| `timeoutSeconds` | `number` | Seconds before the request expires (0/unset = no timeout) |
| `pollSeconds` | `number` | Poll interval while blocking (default 3s) |

**Output:**

```ts
{
  requestId: string;
  runId: string;
  nodeId: string;
  iteration: number;
  status: "answered" | "cancelled" | "expired" | "missing" | "aborted";
  decision: "approved" | "blocked";   // "blocked" => do not proceed
  response: unknown | null;            // the human's answer when status is "answered"
  answeredBy: string | null;
}
```

---

### get_node_detail

Get enriched detail for a single node, including all attempts, tool calls, token usage, scorer results, and validated output.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `runId` | `string` | required |
| `nodeId` | `string` | required |
| `iteration` | `number` | Loop iteration (default: latest) |

**Output:**

```ts
{
  detail: {
    node: { runId, nodeId, iteration, state, lastAttempt, updatedAtMs, outputTable, label };
    status: string;
    durationMs: number | null;
    attemptsSummary: { total, failed, cancelled, succeeded, waiting };
    attempts: unknown[];
    toolCalls: unknown[];
    tokenUsage: unknown;
    scorers: unknown[];
    output: {
      validated: unknown | null;
      raw: unknown | null;
      source: "cache" | "output-table" | "none";
      cacheKey: string | null;
    };
    approval: PendingApproval | null;
    limits: {
      toolPayloadBytesHuman: number;
      validatedOutputBytesHuman: number;
    };
  };
}
```

---

### revert_attempt

Revert the workspace and frame history back to the state captured at a specific attempt. This is destructive and non-idempotent.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run containing the node |
| `nodeId` | `string` | required | Node to revert |
| `iteration` | `number` | `0` | Loop iteration |
| `attempt` | `number` | required | Attempt number to revert to (must be ≥ 1) |

**Output:**

```ts
{
  runId: string;
  nodeId: string;
  iteration: number;
  attempt: number;
  success: boolean;
  error?: string;
  jjPointer?: string;
  run: RunSummary | null;
}
```

---

### fork_run

Create a branched run from a time-travel snapshot checkpoint without starting it.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `parentRunId` | `string` | Source run ID |
| `frameNo` | `number` | Snapshot frame number |
| `resetNodes` | `string[]` | Node IDs to reset to pending in the fork |
| `inputOverrides` | `Record<string, unknown>` | Input fields to overlay on the snapshot input |
| `branchLabel` | `string` | Optional branch label |

**Output:** `{ runId, parentRunId, parentFrameNo, branch, snapshot, run }`

---

### replay_run

Fork a run from a checkpoint for replay, optionally restoring VCS state. Resume the returned `runId` with `run_workflow` when needed.

**Input:** same as `fork_run`, plus:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `restoreVcs` | `boolean` | `false` | Restore the working copy to the source frame revision |
| `cwd` | `string` | - | Working directory used for VCS restore |

**Output:** `{ runId, parentRunId, parentFrameNo, branch, snapshot, vcsRestored, vcsPointer, vcsError?, run }`

---

### rewind_run

Rewind a run to a previous frame, deleting later frames and invalidating derived state. This is destructive and requires `confirm: true`.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `runId` | `string` | Run to rewind |
| `frameNo` | `number` | Target frame number |
| `confirm` | `boolean` | Must be `true` |

**Output:** `{ result, run }`

---

### restore_checkpoint

Restore the worktree to a durability checkpoint for a node. If `seq` is omitted, the latest matching checkpoint is used.

**Input:**

| Parameter | Type | Description |
|---|---|---|
| `runId` | `string` | Run containing the checkpoint |
| `nodeId` | `string` | Node whose checkpoint should be restored |
| `iteration` | `number` | Optional loop iteration |
| `seq` | `number` | Optional checkpoint sequence |

**Output:** `{ runId, nodeId, iteration, seq, commitId, cwd, success, error? }`

---

### list_snapshots

List durability workspace checkpoints for a run with matching VCS operation IDs when available.

**Input:** `{ runId: string }`

**Output:** `{ snapshots: Array<{ seq, nodeId, iteration, attempt, tier, source, label, commitId, operationId, cwd, createdAtMs }> }`

---

### get_timeline

Return the time-travel timeline for a run, optionally including all child forks recursively.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run ID |
| `tree` | `boolean` | `false` | Include child forks recursively |

**Output:** `{ timeline: unknown }`

---

### time_travel

Reset a run back to a prior node attempt and optionally restore VCS state. If the run is still marked running, pass `force: true`.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run ID |
| `nodeId` | `string` | required | Node to travel back to |
| `iteration` | `number` | `0` | Loop iteration |
| `attempt` | `number` | latest | Attempt number |
| `restoreVcs` | `boolean` | `true` | Restore filesystem state |
| `resetDependents` | `boolean` | `true` | Reset dependent nodes too |
| `force` | `boolean` | `false` | Allow time travel when the run is still running |

**Output:** `{ result, run }`

---

### list_artifacts

List structured output artifacts produced by nodes in a run.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run ID |
| `nodeId` | `string` | - | Limit to a specific node |
| `includeRaw` | `boolean` | `false` | Include raw (pre-validation) output values |

**Output:**

```ts
{
  artifacts: Array<{
    artifactId: string;   // "<runId>:<nodeId>:<iteration>"
    kind: "node-output";
    runId: string;
    nodeId: string;
    iteration: number;
    label: string | null;
    state: string;
    outputTable: string | null;
    source: "cache" | "output-table" | "none";
    cacheKey: string | null;
    value: unknown | null;
    rawValue?: unknown | null;   // only when includeRaw=true
  }>;
}
```

Only nodes with an `outputTable` and a non-`none` output source are included.

---

### get_chat_transcript

Return the structured agent chat transcript for a run, grouped by attempts.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run ID |
| `all` | `boolean` | `false` | Include all attempts, not just those with known output events |
| `includeStderr` | `boolean` | `true` | Include stderr messages |
| `tail` | `number` | - | Return only the last N messages |

**Output:**

```ts
{
  runId: string;
  attempts: Array<{
    attemptKey: string;
    nodeId: string;
    iteration: number;
    attempt: number;
    state: string;
    startedAtMs: number;
    finishedAtMs: number | null;
    cached: boolean;
    meta: unknown | null;
  }>;
  messages: Array<{
    id: string;
    attemptKey: string;
    nodeId: string;
    iteration: number;
    attempt: number;
    role: "user" | "assistant" | "stderr";
    stream: "stdout" | "stderr" | null;
    timestampMs: number;
    text: string;
    source: "prompt" | "event" | "responseText";
  }>;
}
```

Messages are sorted by `timestampMs`. Use `tail` to limit context window usage on long transcripts.

---

### get_run_events

Return the raw structured event history for a run with optional filtering.

**Input:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `runId` | `string` | required | Run ID |
| `afterSeq` | `number` | - | Only events with `seq` greater than this value |
| `limit` | `number` (1–10000) | `200` | Max events to return |
| `nodeId` | `string` | - | Filter to events for a specific node |
| `types` | `string[]` | - | Filter to specific event types (e.g. `["NodeFinished", "NodeFailed"]`) |
| `sinceTimestampMs` | `number` | - | Only events at or after this timestamp |

**Output:**

```ts
{
  runId: string;
  events: Array<{
    runId: string;
    seq: number;
    timestampMs: number;
    type: string;
    payload: unknown | null;
  }>;
}
```

Paginate via `afterSeq`: pass the `seq` of the last received event to fetch the next page.

---

## Usage Examples

### List workflows and start a run

```
> list_workflows {}

{
  "ok": true,
  "data": {
    "workflows": [
      { "id": "bugfix", "displayName": "bugfix", "entryFile": "./workflows/bugfix.tsx", "sourceType": "user" }
    ]
  }
}

> run_workflow { "workflowId": "bugfix", "prompt": "Fix the auth token expiry bug" }

{
  "ok": true,
  "data": {
    "runId": "smi_abc123",
    "launchMode": "background",
    "status": "running",
    ...
  }
}
```

### Watch until complete

```
> watch_run { "runId": "smi_abc123", "timeoutMs": 120000 }

{
  "ok": true,
  "data": {
    "reachedTerminal": true,
    "timedOut": false,
    "finalRun": { "status": "finished", ... }
  }
}
```

### Resolve a pending approval

```
> list_pending_approvals { "runId": "smi_abc123" }

{
  "ok": true,
  "data": {
    "approvals": [
      { "nodeId": "deploy", "iteration": 0, "nodeLabel": "Deploy to production", ... }
    ]
  }
}

> resolve_approval { "action": "approve", "runId": "smi_abc123", "nodeId": "deploy", "decidedBy": "alice", "note": "Looks good" }

{
  "ok": true,
  "data": {
    "action": "approve",
    "approval": { "status": "approved", "decidedAtMs": 1707500100000, ... },
    "run": { "status": "running", ... }
  }
}
```

### Debug a blocked run

```
> explain_run { "runId": "smi_abc123" }

{
  "ok": true,
  "data": {
    "diagnosis": {
      "summary": "Run is waiting for a human approval on node 'deploy'.",
      "blockers": [
        {
          "kind": "approval",
          "nodeId": "deploy",
          "reason": "Node requires human approval before proceeding.",
          "unblocker": "Call resolve_approval with action=approve or action=deny."
        }
      ]
    }
  }
}
```

### Revert a failed attempt

```
> get_node_detail { "runId": "smi_abc123", "nodeId": "analyze" }

{
  "ok": true,
  "data": {
    "detail": {
      "attemptsSummary": { "total": 3, "failed": 2, "succeeded": 1 },
      ...
    }
  }
}

> revert_attempt { "runId": "smi_abc123", "nodeId": "analyze", "attempt": 1 }

{
  "ok": true,
  "data": {
    "success": true,
    "run": { "status": "running", ... }
  }
}
```

---

## Error Codes

Errors follow the structured envelope. Common codes:

| Code | Meaning |
|---|---|
| `RUN_NOT_FOUND` | No run or workflow exists with the given ID |
| `INVALID_INPUT` | Missing required field, failed validation, or ambiguous approval filter |
| `WORKFLOW_MISSING_DEFAULT` | Workflow file has no default export |


===============================================================================

# Smithers Effect API

> Smithers Effect-ts authoring API: build workflows as Effect values without JSX or React.

---

## Effect API

> Build Smithers workflows as first-class graph values, no JSX or React.

The Effect API is the lower-level authoring surface for teams that already model
application logic with `Effect`, `Layer`, and `Schema`. It uses the same Smithers
runtime as JSX: steps are persisted in SQLite, completed work is not re-run on
resume, outputs are schema-validated, and dependencies drive scheduling. The
difference is authoring style: every step, approval, sequence, parallel block,
match, branch, loop, worktree, and scope is an ordinary value you can export,
return from a function, or compose with other graph values.

Use JSX for most workflows. Use the Effect API when your workflow lives inside
an Effect service, you want step bodies to return `Effect` values directly, or
you need a React-free API for generated workflow definitions.

## Minimal workflow

```ts
import { Smithers } from "smithers-orchestrator";
import { Effect, Schema } from "effect";

const inputSchema = Schema.Struct({
  repo: Schema.String,
  sha: Schema.String,
});

const analysisSchema = Schema.Struct({
  summary: Schema.String,
  risk: Schema.Literal("low", "medium", "high"),
});

const reportSchema = Schema.Struct({
  markdown: Schema.String,
});

const G = Smithers.workflow({
  name: "repo-review",
  input: inputSchema,
});

const analyze = G.step("analyze", {
  output: analysisSchema,
  timeout: "2m",
  retry: { maxAttempts: 3, backoff: "exponential", initialDelay: "1s" },
  run: ({ input, heartbeat }) =>
    Effect.gen(function* () {
      heartbeat({ phase: "analyzing" });
      yield* Effect.log(`Reviewing ${input.repo}@${input.sha}`);
      return { summary: "Found one risky migration.", risk: "medium" as const };
    }),
});

const report = G.step("report", {
  needs: { analyze },
  output: reportSchema,
  run: ({ analyze }) => ({
    markdown: `# Review\n\n${analyze.summary}\n\nRisk: ${analyze.risk}`,
  }),
});

export const reviewWorkflow = G.from(G.sequence(analyze, report));

const result = await Effect.runPromise(
  reviewWorkflow
    .execute(
      { repo: "acme/api", sha: "abc123" },
      { runId: "review-abc123" },
    )
    .pipe(Effect.provide(Smithers.sqlite({ filename: "smithers.db" }))),
);
```

`Smithers.workflow(opts)` returns a typed handle `G`. Every constructor
(`G.step`, `G.approval`, `G.sequence`, `G.parallel`, `G.match`, `G.branch`,
`G.loop`, `G.worktree`, `G.scope`) returns a graph value. `G.from(graph)`
finalizes the workflow into something `execute`-able.

`execute()` returns an `Effect`. The success value is the decoded output of the
final graph node: a step output for a single step, the last child for a
sequence, a tuple for a parallel block. If the run stops on an approval or
timer, the success value is the normal `RunResult` with a waiting status.

## Steps and dependencies

Steps are values:

```ts
const analyze = G.step("analyze", {
  output: analysisSchema,
  run: ({ input }) => analyzeRepo(input.repo, input.sha),
});
```

`input` is typed from the workflow's input schema. The step's output type is
inferred from the `output` schema and flows into anything that lists this step
in `needs`:

```ts
const report = G.step("report", {
  needs: { analyze },
  output: reportSchema,
  run: ({ analyze }) => ({
    markdown: renderReport(analyze.summary, analyze.risk),
  }),
});
```

Step IDs are durable. Changing an ID creates a new task and leaves the old
persisted output behind. A step can return a plain value, a `Promise`, or an
`Effect`; Smithers decodes the result with the step's `output` schema before
writing it.

The step context includes `input`, dependency values, `executionId`, `stepId`,
`attempt`, `iteration`, `signal`, `heartbeat(data)`, and `lastHeartbeat`.

## Control flow

Use `G.sequence(...nodes)` for ordered work and
`G.parallel(...nodes, { maxConcurrency })` for concurrent work. `G.parallel`
returns a tuple of child results.

`G.match(source, { when, then, else })` selects between two statically-known
branches based on a completed step's output. Both branches are compiled into
the graph; only the matching branch executes.

`G.branch({ condition, needs, then, else })` is the same shape but the
predicate runs against an arbitrary `needs` context, not a single source step.

`G.loop({ id, children, until, maxIterations, onMaxReached })` repeats a fragment until the
predicate returns true. Nested loops are not supported. `onMaxReached` accepts `'fail'` or `'return-last'` and controls behavior when `maxIterations` is exceeded; when omitted the loop returns the last iteration's outputs rather than failing.

```ts
export const reviewWorkflow = G.from(
  G.sequence(
    analyze,
    G.match(analyze, {
      when: (analysis) => analysis.risk === "high",
      then: G.approval("approve-high-risk", {
        needs: { analyze },
        request: ({ analyze }) => ({
          title: "Approve high-risk review",
          summary: analyze.summary,
        }),
        onDeny: "fail",
      }),
      // else: report (omitting `else` means the match falls through to the next sibling in the sequence)
    }),
    report,
  ),
);
```

## Worktrees

`G.worktree({ id, path, branch, skipIf, needs, children })` runs `children`
inside a git worktree. The worktree is created before the children execute and
torn down afterward.

```ts
G.worktree({
  id: "review-shard",
  path: "scratch/review",
  children: G.sequence(read, summarize),
});
```

## Reuse

Static reuse is just a graph value:

```ts
const reviewShard = G.sequence(read, summarize);
```

Parameterized reuse is a function returning a graph value:

```ts
const makeReviewShard = (params: { path: string }) =>
  G.sequence(
    G.step("read", {
      output: diffSchema,
      run: ({ input }) => readDiff(input.repo, params.path),
    }),
    G.step("summarize", {
      needs: { read },
      output: summarySchema,
      run: ({ read }) => summarizeDiff(read),
    }),
  );
```

Multi-mount reuse is `G.scope(instanceId, fragment)`. The compiler applies
`instanceId.` as a durable ID prefix to every step and approval inside the
fragment. For example, `G.scope('api', makeReviewShard(...))` produces step IDs `api.read` and `api.summarize` in the database. The same fragment can be mounted under multiple scopes without collision:

```ts
G.parallel(
  G.scope("api", makeReviewShard({ path: "packages/api" })),
  G.scope("web", makeReviewShard({ path: "apps/web" })),
);
```

## Cross-workflow fragments

For graph fragments that need to live across workflows with different inputs,
build them with `Smithers.fragment(inputSchema)`:

```ts
const F = Smithers.fragment(diffInputSchema);

const readDiff = F.step("read-diff", {
  output: diffSchema,
  run: ({ input }) => readDiff(input.path),
});

const summarize = F.step("summarize", {
  needs: { readDiff },
  output: summarySchema,
  run: ({ readDiff }) => summarizeDiff(readDiff),
});

export const reviewShard = F.sequence(readDiff, summarize);
```

`Smithers.fragment` exposes the same constructors as a workflow handle (`step`,
`approval`, `sequence`, `parallel`, `match`, `branch`, `loop`, `worktree`,
`scope`), but no `from`; fragments are values, not workflows. Compile happens
when they're mounted into a real workflow:

```ts
const G = Smithers.workflow({ name: "repo-review", input: workflowInputSchema });

export const reviewWorkflow = G.from(
  G.parallel(
    G.scope("api", reviewShard),
    G.scope("web", reviewShard),
  ),
);
```

A fragment is typed with an input schema so TypeScript can infer each step's `input` type. At runtime the schema is not read or validated; steps receive the host workflow's input directly. The host workflow's input type must be assignable to the fragment's input schema. This is enforced at compile time: TypeScript will error if you mount a fragment whose input schema has fields the host workflow's input doesn't satisfy.

## Operational notes

- Provide exactly one persistence layer with `Effect.provide(Smithers.sqlite({ filename }))`.
- Keep step IDs stable across releases; use new IDs for materially different work.
- Use `heartbeat()` in long-running steps and honor `signal` in external calls.
- Use `retry`, `retryPolicy`, `timeout`, `skipIf`, and `cache` the same way you
  would on JSX tasks (see JSX Task options for the shared option shape).
- All graph values support `.pipe(...fns)` for future data-last combinators.
- Prefer idempotent step bodies. For external side effects, use `executionId`,
  `stepId`, and `attempt` when constructing idempotency keys.
- `G.match` is graph topology selection: both branches must be statically
  knowable so durable IDs stay stable across resume. It is not Effect's
  `Match` module (which is runtime value pattern matching).


===============================================================================

# Smithers Integrations

> Smithers integrations: agent runtimes (Claude Code, Codex, Gemini, Pi), tool surfaces, ecosystem partners.

---

## Integrations

> Three patterns for connecting Smithers to external services.

Smithers ships first-party event sources for GitHub, Linear, and Telegram (the `@smithers-orchestrator/integrations` package) that turn provider webhooks into durable workflow signals. For services it does not cover (Notion, Slack, and similar), treat them as external integrations your app owns and pick one of three wirings: tools on an SDK agent, skills/plugins/MCP on a CLI agent, or an external CLI run inside a task. If a service already exposes an MCP server and you are using an SDK agent, MCP Toolset turns that server into AI SDK tools.

## Pattern 1: Pass tools to an SDK agent

Use this when the agent needs judgment but the external calls should stay explicit and reviewable.

```ts
import { ToolLoopAgent as Agent, tool, zodSchema } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const linearGetIssue = tool({
  description: "Fetch a Linear issue",
  inputSchema: zodSchema(z.object({ id: z.string() })),
  execute: async ({ id }) => linearClient.getIssue(id),
});

const opsAgent = new Agent({
  model: anthropic("claude-fable-5"),
  tools: { linearGetIssue },
});
```

## Pattern 2: Pass a skill / plugin / MCP config to a CLI agent

Use this when your CLI agent already supports external integrations and Smithers should only orchestrate the task.

```ts
import { PiAgent } from "smithers-orchestrator";

const pi = new PiAgent({
  provider: "openai",
  model: "gpt-5.6-sol",
  skill: ["./skills/linear", "./skills/notion"],
});
```

```tsx
<Task id="ticket-review" output={outputs.review} agent={pi}>
  {`Use the Linear skill to inspect ${ctx.input.issueId}, then summarize next actions.`}
</Task>
```

## Pattern 3: Run an external CLI in a task

Use this when the step is deterministic and you do not need the model involved.

```tsx
<Task id="load-linear" output={outputs.linearIssue}>
  {async () => {
    const proc = Bun.spawn(["linear", "issue", "view", ctx.input.issueId, "--json"], {
      stdout: "pipe",
      stderr: "pipe",
    });
    const stdout = await new Response(proc.stdout).text();
    const stderr = await new Response(proc.stderr).text();
    if (await proc.exited !== 0) throw new Error(stderr || stdout);
    return JSON.parse(stdout);
  }}
</Task>
```

## Choosing between them

| If you need | Prefer |
|---|---|
| AI judgment over a small integration surface | Pattern 1 (SDK agent with narrow tools) |
| Existing CLI ecosystem support (skills, plugins, MCP) | Pattern 2 (CLI agent) |
| Deterministic sync or publish steps | Pattern 3 (compute task) |


---

## CLI Agents

> Spawn external CLI tools (Codex, Claude Code, Pi, …) and pipe them through the workflow runtime.

CLI-backed agent classes wrap external AI command-line tools and implement the [AI SDK](https://ai-sdk.dev) `Agent` interface. Use them anywhere Smithers accepts an agent, including `<Task>`. Reach for these for a vendor's full CLI surface (sessions, sandboxes, slash commands, MCP). For API-billed provider wrappers, see SDK Agents.

<Note>API reference: Agents lists every agent class, its options, and links to source and tests.</Note>

## Quick Start

```tsx
import { CodexAgent, Task, Workflow, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  analysis: z.object({ summary: z.string() }),
});

const reviewer = new CodexAgent({
  model: "gpt-5.6-sol",
  systemPrompt: "You are a careful code reviewer.",
  sandbox: "read-only",
  timeoutMs: 30 * 60 * 1000,
});

export default smithers((ctx) => (
  <Workflow name="review">
    <Task id="analysis" output={outputs.analysis} agent={reviewer}>
      {`Analyze the codebase and identify potential improvements.`}
    </Task>
  </Workflow>
));
```

## Available agents

```toon
agents[12]{class,cli,modelDefault,hijack,notes}:
  CodexAgent,codex,CLI default,native thread id,OpenAI Codex CLI (codex exec via stdin + JSON stream)
  ClaudeCodeAgent,claude,CLI default,native session id,Anthropic Claude Code CLI
  AntigravityAgent,agy,CLI default,native session id,Google Antigravity CLI
  GeminiAgent,gemini,CLI default,legacy session id,Deprecated legacy Gemini CLI wrapper
  PiAgent,pi,CLI default,native session id,Pi CLI (text/json/rpc modes + extension UI hooks)
  KimiAgent,kimi,CLI default,native session id,Moonshot Kimi CLI (auto-isolates KIMI_SHARE_DIR)
  ForgeAgent,forge,CLI default,conversation id,Forge CLI (300+ models)
  HermesCliAgent,hermes,CLI default,session id,Nous Research Hermes Agent CLI (hermes -z headless)
  AmpAgent,amp,CLI default,thread id,Amp CLI (--execute headless mode)
  VibeAgent,vibe,CLI default,headless session id,Mistral Vibe CLI
  OpenCodeAgent,opencode,CLI default,not yet,OpenCode CLI (opencode run --format json)
  OpenClawAgent,openclaw,CLI default,session id,OpenClaw CLI (openclaw agent --json)
```

CLI binaries must be on `PATH`: `codex`, `claude`, `agy`, `gemini`, `pi`, `kimi`, `forge`, `hermes`, `amp`, `vibe`, `opencode`, `openclaw`.

`HermesCliAgent` drives the Hermes *agent* CLI. It is distinct from `HermesAgent`,
which talks to the Hermes *model* over an OpenAI-compatible HTTP API. Use
`HermesCliAgent` to delegate a task to the Hermes coding agent itself; use
`HermesAgent` to call a Hermes model endpoint.

`OpenClawAgent` drives `openclaw agent` in JSON mode. Pass `agent` for a named
OpenClaw agent, `sessionId` to resume a session with `--session-id`, `workspace`
to set the workspace path, and `continueSession` when the installed CLI should
continue its active session.

## Codex-first defaults

When `bunx smithers-orchestrator init` detects usable Codex authentication, its
generated pools start with Codex workers using exact model pins:

| Work | Model |
|---|---|
| Planning, orchestration, review, and the hardest reasoning | `gpt-5.6-sol` |
| Mid-tier judgment, validation, or tool-heavy work that needs extra checking | `gpt-5.6-terra` |
| **Research, implementation, ordinary tool use, UI, and routine-to-substantial execution** | `gpt-5.6-luna` |

Start with Luna for most work, including substantial implementation and
ordinary tool use; change size alone does not require Sol. Escalate to Terra
for stronger validation or structured tool-heavy judgment, and to Sol for
ambiguity, high-stakes decisions, novel architecture, orchestration, final
review, or repeated failure. Other adapters stay later in automatic sequential
fallback chains, while an explicitly constructed provider agent remains a
deliberate choice. See SOTA role defaults.

## Codex CLI Agent

`CodexAgent` is the Smithers wrapper for OpenAI's `codex` CLI. It runs `codex exec` in non-interactive mode, sends the task prompt over stdin, forces `--json` so Smithers can stream structured progress, and captures the final assistant message via `--output-last-message`.

```tsx
import { CodexAgent, Task, Workflow, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { smithers, outputs } = createSmithers({
  patch: z.object({ summary: z.string(), files: z.array(z.string()) }),
});

const codex = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  sandbox: "workspace-write",
  skipGitRepoCheck: true,
  yolo: true,
});

export default smithers(() => (
  <Workflow name="codex-implementation">
    <Task id="implement" output={outputs.patch} agent={codex}>
      Implement the requested change and summarize the edited files.
    </Task>
  </Workflow>
));
```

For ChatGPT-account Codex auth, use an exact Codex model id such as
`gpt-5.6-luna`; do not invent a `-codex` suffix.

### Authentication

- Subscription login: run `codex login` once. For isolated accounts, pass `configDir`; Smithers sets `CODEX_HOME` for that invocation.
- API billing: pass `apiKey` or set `OPENAI_API_KEY`; Smithers forwards it to the spawned `codex` process.
- Account registry: `bunx smithers-orchestrator agents add --provider codex ...` registers a subscription config directory, while `--provider openai-api` registers API-key billing for Codex-compatible providers.

### Structured output

- If the Smithers task has an output schema and `outputSchema` is not set, Smithers writes a temporary OpenAI-compatible JSON Schema file and passes it as `--output-schema`.
- Resume attempts use `codex exec resume <thread-id>` and skip `--output-schema`, matching the Codex CLI's resume command surface.
- Hijack opens native Codex with `codex resume <thread-id> -C <cwd>`.

## Claude Code CLI Agent

`ClaudeCodeAgent` is the Smithers wrapper for Anthropic's `claude` CLI. It runs
the CLI non-interactively, captures the final assistant message, and (by default)
forces `--output-format stream-json` so Smithers can stream structured progress.

### Authentication

- Subscription billing (default): `ClaudeCodeAgent` clears `ANTHROPIC_API_KEY`
  from the spawned process so the CLI bills your Claude Pro/Max subscription
  instead of the API. No key is required. The agent logs a one-time warning when
  it unsets an inherited `ANTHROPIC_API_KEY`.
- Subscription login: the `claude` CLI stores credentials per config directory.
  To set up an isolated subscription, run `CLAUDE_CONFIG_DIR=<dir> claude` once
  and complete `/login` interactively. The credentials land at
  `<dir>/.credentials.json`.
- Pinning a subscription: pass `configDir` to use that directory's credentials.
  Smithers sets `CLAUDE_CONFIG_DIR=<configDir>` for that invocation, so you can
  run several subscriptions side by side. Omit it to use the default `~/.claude/`.
- API billing: pass `apiKey` to bill the Anthropic API instead. When `apiKey` is
  set, Smithers stops clearing `ANTHROPIC_API_KEY` and forwards your key as
  `ANTHROPIC_API_KEY` to the spawned `claude` process.

```ts
import { ClaudeCodeAgent } from "smithers-orchestrator";

// Subscription billing, pinned to an isolated login dir (no API key):
const claude = new ClaudeCodeAgent({
  model: "claude-fable-5",
  configDir: "/home/me/.claude-work", // CLAUDE_CONFIG_DIR for this invocation
});

// API billing (switches off subscription auth):
const apiClaude = new ClaudeCodeAgent({
  model: "claude-fable-5",
  apiKey: process.env.ANTHROPIC_API_KEY,
});
```

This mirrors the Codex authentication surface
(`codex login` / `CODEX_HOME` via `configDir`, `OPENAI_API_KEY` via `apiKey`).

## Subscription-mode structured completion

You do not need a Workflow or Task
graph to call a model once and get a typed object back. Construct a CLI agent and
call `agent.generate({ prompt, outputSchema, timeout, abortSignal })` directly.
With no `apiKey`, the call bills the host subscription, returns a single
completion, and is bounded by `timeout` and `abortSignal`.

`generate()` resolves to an AI SDK `GenerateTextResult`. Read the full text from
`.text`; when the response is valid JSON, Smithers parses it into `.output`
using the AI SDK 7 structured-output field.

### Claude Code (subscription, no API key)

`ClaudeCodeAgent.generate()` does not auto-inject the schema into the prompt
(that injection happens inside `<Task>`), so when calling it standalone, instruct
JSON in the prompt yourself. `outputSchema` drives `.output` parsing and
validation. Setting `outputFormat: "json"` and `tools: ""` keeps the run a single
quiet completion with no tool use.

```ts
import { ClaudeCodeAgent } from "smithers-orchestrator";
import { z } from "zod";

const schema = z.object({
  sentiment: z.enum(["positive", "neutral", "negative"]),
  summary: z.string(),
});

const claude = new ClaudeCodeAgent({
  model: "claude-fable-5",
  outputFormat: "json",
  tools: "", // no tools, pure completion
  // no apiKey → bills your Claude Pro/Max subscription
});

const controller = new AbortController();
const result = await claude.generate({
  prompt:
    "Classify the sentiment of this review and summarize it in one sentence. " +
    'Respond with ONLY a raw JSON object: {"sentiment": "...", "summary": "..."}. ' +
    "First character `{`, last character `}`, no prose or code fences.\n\n" +
    "Review: Shipping was slow but the product is excellent.",
  outputSchema: schema,
  timeout: { totalMs: 2 * 60 * 1000, idleMs: 30 * 1000 },
  abortSignal: controller.signal,
});

const parsed = schema.parse(result.output ?? JSON.parse(result.text));
console.log(parsed.sentiment, parsed.summary);
```

### Codex (subscription, strict JSON)

`CodexAgent` with `nativeStructuredOutput: true` forwards the schema to the CLI
as `codex exec --output-schema`, so the model is constrained to emit JSON
matching the schema. No API key is needed; it bills your ChatGPT subscription
via `~/.codex/auth.json` (or `CODEX_HOME` when `configDir` is set).

```ts
import { CodexAgent } from "smithers-orchestrator";
import { z } from "zod";

const schema = z.object({
  sentiment: z.enum(["positive", "neutral", "negative"]),
  summary: z.string(),
});

const codex = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  nativeStructuredOutput: true, // forwards outputSchema as --output-schema
  // no apiKey → bills your ChatGPT subscription
});

const result = await codex.generate({
  prompt:
    "Classify the sentiment of this review and summarize it in one sentence.\n\n" +
    "Review: Shipping was slow but the product is excellent.",
  outputSchema: schema,
  timeout: { totalMs: 2 * 60 * 1000, idleMs: 30 * 1000 },
});

const parsed = schema.parse(result.output);
console.log(parsed.sentiment, parsed.summary);
```

Notes:

- No API key is required for either agent; the call bills the host subscription.
- `outputSchema` is honored differently per agent: Codex constrains decoding via
  `--output-schema` (strict JSON); Claude Code relies on the JSON you ask for in
  the prompt and parses it into `.output`. Both validate against the schema.
- A single `generate()` call returns one completion. There is no graph, no
  durability, and no retry loop unless you add one. For schema-validation retries,
  durability, and multi-step orchestration, wrap the agent in a `<Task>`.
- `timeout: { totalMs, idleMs }` caps wall-clock and idle time; pass an
  `AbortSignal` to cancel from the outside.

## Common options

All CLI agents accept the same base option surface:

```ts
type BaseCliAgentOptions = {
  id?: string;                       // Agent instance id (default: random UUID)
  model?: string;                    // Model name passed to --model
  systemPrompt?: string;             // Prepended to the user prompt
  instructions?: string;             // Alias for systemPrompt
  cwd?: string;                      // Working directory (default: tool ctx rootDir or process.cwd())
  env?: Record<string, string>;      // Extra env vars merged with process.env
  yolo?: boolean;                    // Skip permission prompts (default: true)
  timeoutMs?: number;                // Hard wall-clock cap
  idleTimeoutMs?: number;            // Inactivity cap; resets on any stdout/stderr
  maxOutputBytes?: number;           // Truncate captured output
  extraArgs?: string[];              // Additional CLI flags appended to the command
};
```

Per-call timeout override:

```ts
await agent.generate({
  prompt: "do the thing",
  timeout: { totalMs: 15 * 60 * 1000, idleMs: 2 * 60 * 1000 },
});
```

## Per-agent extras

`ClaudeCodeAgent` extends the base with Claude Code-specific session and permission flags. Key additions: `permissionMode`, `sessionId`, `mcpConfig`, `resume`.

```ts
import { ClaudeCodeAgent } from "smithers-orchestrator";
new ClaudeCodeAgent({
  permissionMode?: "acceptEdits" | "bypassPermissions" | "default" | "delegate" | "dontAsk" | "plan";
  allowedTools?: string[]; disallowedTools?: string[]; disableSlashCommands?: boolean;
  addDir?: string[]; file?: string[]; fromPr?: string; fallbackModel?: string;
  appendSystemPrompt?: string; agents?: Record<string, { description?: string; prompt?: string }> | string;
  agent?: string; tools?: string[] | "default" | "";
  betas?: string[]; pluginDir?: string[]; resume?: string; sessionId?: string;
  mcpConfig?: string[]; mcpDebug?: boolean; maxBudgetUsd?: number; jsonSchema?: string;
  configDir?: string; apiKey?: string;
  dangerouslySkipPermissions?: boolean; allowDangerouslySkipPermissions?: boolean; chrome?: boolean; noChrome?: boolean;
  continue?: boolean; forkSession?: boolean; noSessionPersistence?: boolean; replayUserMessages?: boolean;
  debug?: boolean | string; debugFile?: string; ide?: boolean; includePartialMessages?: boolean;
  inputFormat?: "text" | "stream-json"; settingSources?: string; settings?: string; strictMcpConfig?: boolean; verbose?: boolean;
  outputFormat?: "text" | "json" | "stream-json"; // default stream-json
});
```

#### Allow-list and deny-list tools

`allowedTools` and `disallowedTools` are independent string arrays that map to
the Claude Code CLI's `--allowed-tools` and `--disallowed-tools`. Use them
together: `allowedTools` whitelists what the agent may use, and `disallowedTools`
hard-blocks tools even if they would otherwise be allowed. To let an agent read
and write files but never run a shell, list both:

```ts
const reviewer = new ClaudeCodeAgent({
  model: "claude-fable-5",
  allowedTools: ["Read", "Write"],   // may read and write files
  disallowedTools: ["Bash"],          // but never execute shell commands
});
```

Setting `allowedTools` alone does not block `Bash`; the deny-list is what
forbids it. Tool names follow the Claude Code convention (`Read`, `Write`,
`Edit`, `Bash`, `WebFetch`, `Grep`, ...) and `Bash(git:*)`-style scoping is
allowed.

#### MCP servers (`mcpConfig`)

`mcpConfig` is a string array passed straight through to the CLI's `--mcp-config`
flag. Each entry is **either a path to an MCP config JSON file or an inline JSON
string** (the Claude Code CLI accepts both forms), so you can mix them:

```ts
const agent = new ClaudeCodeAgent({
  model: "claude-fable-5",
  mcpConfig: [
    "./.mcp.json",                                                   // a file path
    '{"mcpServers":{"fs":{"command":"npx","args":["@modelcontextprotocol/server-filesystem","."]}}}', // inline JSON
  ],
  strictMcpConfig: true, // ignore any other MCP sources, use only mcpConfig
});
```

`CodexAgent` extends the base with OpenAI Codex-specific flags. Key additions: `sandbox`, `config`, `outputSchema`.

```ts
import { CodexAgent } from "smithers-orchestrator";
new CodexAgent({
  sandbox?: "read-only" | "workspace-write" | "danger-full-access";
  fullAuto?: boolean; dangerouslyBypassApprovalsAndSandbox?: boolean;
  config?: Record<string, string | number | boolean | object | null> | string[];
  enable?: string[]; disable?: string[];
  oss?: boolean; localProvider?: string;
  image?: string[]; profile?: string; cd?: string; addDir?: string[];
  skipGitRepoCheck?: boolean; color?: "always" | "never" | "auto";
  outputSchema?: string; outputLastMessage?: string; json?: boolean;
  configDir?: string; apiKey?: string;
});
```

`AntigravityAgent` wraps the Google `agy` CLI. Key additions: `allowedMcpServerNames`, `geminiDir`, `conversation`, `continue`, and `resume`.

```ts
import { AntigravityAgent } from "smithers-orchestrator";
new AntigravityAgent({
  model?: string; sandbox?: boolean;
  yolo?: boolean; dangerouslySkipPermissions?: boolean;
  allowedMcpServerNames?: string[]; allowedTools?: string[];
  conversation?: string; continue?: boolean; resume?: string;
  includeDirectories?: string[];
  extensions?: string[]; listExtensions?: boolean;
  listSessions?: boolean; deleteSession?: string;
  screenReader?: boolean; outputFormat?: "text" | "json" | "stream-json";
  debug?: boolean;
  binary?: string; configDir?: string; geminiDir?: string; apiKey?: string;
  // Deprecated and rejected at runtime: debug, screenReader, outputFormat,
  // extensions, listExtensions, listSessions, deleteSession.
});
```

Current `agy` builds changed several Gemini-era flags. Smithers treats that as a
runtime contract, not a best-effort pass-through:

| Smithers option | Emitted `agy` surface |
|---|---|
| `includeDirectories` | `--add-dir` |
| `conversation` / `resume` | `--conversation <id>` |
| `continue` | `--continue` |
| `configDir` / `geminiDir` | `--gemini_dir <dir>` and `GEMINI_DIR=<dir>` |
| prompt text | `-p <prompt>` |

Smithers does not emit `--output-format`, `--include-directories`, `--resume`,
`--screen-reader`, `--debug`, extension flags, session-list flags, or
`--prompt` for Antigravity. Options that would require those removed flags fail
fast with `AGENT_CONFIG_INVALID` and a replacement hint. Plugins are managed
outside workflow launch through `agy plugin`.

`GeminiAgent` is the deprecated legacy wrapper for the older `gemini` CLI. Prefer `AntigravityAgent` for new Google CLI integrations, but existing workflows can still use `GeminiAgent`.

```ts
import { GeminiAgent } from "smithers-orchestrator";
new GeminiAgent({
  debug?: boolean; model?: string; sandbox?: boolean; yolo?: boolean;
  approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
  experimentalAcp?: boolean;
  allowedMcpServerNames?: string[]; allowedTools?: string[];
  extensions?: string[]; listExtensions?: boolean;
  resume?: string; listSessions?: boolean; deleteSession?: string;
  includeDirectories?: string[]; screenReader?: boolean;
  outputFormat?: "text" | "json" | "stream-json";
  configDir?: string; apiKey?: string;
});
```

`PiAgent` wraps the Pi CLI and adds extension UI hook support. Key additions: `provider`, `model`, `mode`, `onExtensionUiRequest`, `extension`, `thinking`.

```ts
import { PiAgent, type PiExtensionUiRequest, type PiExtensionUiResponse } from "smithers-orchestrator";
new PiAgent({
  provider?: string; model?: string; apiKey?: string; appendSystemPrompt?: string; mode?: "text" | "json" | "rpc";
  print?: boolean; continue?: boolean; resume?: boolean; session?: string;
  sessionDir?: string; noSession?: boolean;
  models?: string | string[]; listModels?: boolean | string;
  extension?: string[]; skill?: string[]; promptTemplate?: string[]; theme?: string[];
  noExtensions?: boolean; noSkills?: boolean; noPromptTemplates?: boolean; noThemes?: boolean;
  tools?: string[]; noTools?: boolean; files?: string[];
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
  export?: string; verbose?: boolean;
  onExtensionUiRequest?: (req: PiExtensionUiRequest) => Promise<PiExtensionUiResponse | null> | PiExtensionUiResponse | null;
});
```

`KimiAgent` wraps the Moonshot Kimi CLI with automatic session isolation. Key additions: `thinking`, `agent`, `maxRalphIterations`.

```ts
import { KimiAgent } from "smithers-orchestrator";
new KimiAgent({
  thinking?: boolean; outputFormat?: "text" | "stream-json";
  finalMessageOnly?: boolean; quiet?: boolean;
  agent?: "default" | "okabe"; agentFile?: string;
  workDir?: string; session?: string; continue?: boolean;
  skillsDir?: string; mcpConfigFile?: string[]; mcpConfig?: string[];
  maxStepsPerTurn?: number; maxRetriesPerStep?: number; maxRalphIterations?: number;
  verbose?: boolean; debug?: boolean; configDir?: string; // sets KIMI_SHARE_DIR (see below)
});
```

Unlike the other agents, `KimiAgent`'s `configDir` sets the **`KIMI_SHARE_DIR`**
environment variable for that invocation (Kimi has no separate config-dir flag),
pinning credentials and session state to `<configDir>` instead of the default
`~/.kimi/`. It is exactly equivalent to `env: { KIMI_SHARE_DIR: "<configDir>" }`;
the `configDir` name just keeps the option uniform across agents.

`ForgeAgent` wraps the Forge CLI and supports 300+ models via provider/model strings. Key additions: `conversationId`, `provider`, `workflow`.

```ts
import { ForgeAgent } from "smithers-orchestrator";
new ForgeAgent({
  directory?: string; provider?: string; agent?: string;
  conversationId?: string; sandbox?: string; restricted?: boolean;
  verbose?: boolean; workflow?: string; event?: string; conversation?: string;
});
```

`HermesCliAgent` wraps the Nous Research Hermes Agent CLI in its headless one-shot
mode (`hermes -z "<prompt>"`): a single prompt in, the final response text out.
Key additions: `provider`, `continueSession`. A per-call `resumeSession` emits
`-r <session>`; a configured `continueSession` emits `-c [name]`.

```ts
import { HermesCliAgent } from "smithers-orchestrator";
new HermesCliAgent({
  model?: string; provider?: string;
  continueSession?: string | boolean;
});
```

Smithers also installs a native Hermes plugin (slash commands, a live-run status
injector, lifecycle hooks, a bundled skill, and approval buttons) when you run
`bunx smithers-orchestrator hermes` (an alias for `bunx smithers-orchestrator mcp add --agent hermes`),
so Hermes can drive Smithers as its durable control plane. See the
Hermes & Eliza page.

`OpenClawAgent` wraps the OpenClaw gateway-backed agent CLI via
`openclaw agent --message <prompt> --json`. Key additions: `agent`, `session`,
`workspace`, `json`, and `continueSession`. A per-call `resumeSession` emits
`--session-id <id>`, so Smithers can route retries or continuations into the right
OpenClaw conversation when the installed CLI supports sessions.

```ts
import { OpenClawAgent } from "smithers-orchestrator";
new OpenClawAgent({
  agent?: string;
  session?: string;
  workspace?: string;
  json?: boolean;
  continueSession?: boolean;
});
```

Smithers also installs a native OpenClaw plugin when you run
`bunx smithers-orchestrator mcp add --agent openclaw`. The plugin adds Smithers
workflow tools, a bundled orchestration skill, and eval/optimization tools that
encourage OpenClaw to turn repeated work into reusable, measured workflows. See
OpenClaw.

`AmpAgent` wraps the Amp CLI in `--execute` headless mode. Key additions: `visibility`, `mcpConfig`, `dangerouslyAllowAll`.

```ts
import { AmpAgent } from "smithers-orchestrator";
new AmpAgent({
  visibility?: "private" | "public" | "workspace" | "group";
  mcpConfig?: string; settingsFile?: string;
  logLevel?: "error" | "warn" | "info" | "debug" | "audit"; logFile?: string;
  dangerouslyAllowAll?: boolean; ide?: boolean; jetbrains?: boolean;
});
```

`VibeAgent` wraps Mistral's `vibe` CLI with streaming JSON output. Key additions: `agent`, `maxTurns`, `maxPrice`, `maxTokens`, `enabledTools`, `sessionId`, `continueSession`.

```ts
import { VibeAgent } from "smithers-orchestrator";
new VibeAgent({
  agent?: string;
  maxTurns?: number; maxPrice?: number; maxTokens?: number;
  enabledTools?: string[];
  sessionId?: string; continueSession?: boolean;
});
```

`OpenCodeAgent` wraps the OpenCode CLI via `opencode run --format json`. Key additions: `agentName`, `continueSession`, `sessionId`. Note: native hijack support is not yet shipped.

```ts
import { OpenCodeAgent } from "smithers-orchestrator";
new OpenCodeAgent({
  model?: string; agentName?: string;
  attachFiles?: string[];
  continueSession?: boolean; sessionId?: string;
  variant?: string;
});
```

## Hijack handoff

Most built-in CLI agents support `bunx smithers-orchestrator hijack RUN_ID`, which relaunches the agent in its native CLI session for interactive takeover.

Smithers persists the native session or conversation id on each task event. On hijack, it waits for a safe boundary between blocking tool calls, then reopens the session via the vendor's resume flag:

| Agent class | Resume flag |
|---|---|
| `ClaudeCodeAgent` | `claude --resume` |
| `CodexAgent` | `codex resume` |
| `AntigravityAgent` | `agy --conversation` |
| `PiAgent` | `pi --session` |
| `KimiAgent` | `kimi --session` |
| `ForgeAgent` | `forge --conversation-id` |
| `AmpAgent` | `amp threads continue` |

On clean exit the workflow resumes in detached mode. Cursor, Vibe, OpenCode, and OpenClaw stream capture and headless session continuation are documented above, but native `bunx smithers-orchestrator hijack` support for Cursor, Vibe, OpenCode, and OpenClaw is not shipped yet. The deprecated `GeminiAgent` has no native hijack launcher; use `AntigravityAgent` (`agy --conversation`) for Google CLI takeover. See How it works → Durability and resume.

## Notes

- **Yolo defaults.** `yolo: true` (default) maps to each CLI's "skip approvals" flag (`--dangerously-skip-permissions`, `--dangerously-bypass-approvals-and-sandbox`, `--yolo`, `--dangerously-allow-all`). Set `yolo: false` or use the agent-specific approval option for tighter control.
- **Pi rpc mode** sends prompts as JSON over stdin and is required for `onExtensionUiRequest` callbacks; text/json modes pass the prompt as a positional arg with `files` emitted as `@path`.
- **Kimi share dir.** `KimiAgent` auto-creates an isolated `KIMI_SHARE_DIR` per invocation to prevent `kimi.json` corruption under concurrent runs. Pass `configDir` (or `env.KIMI_SHARE_DIR`) to pin a specific directory instead.
- **Antigravity config.** `AntigravityAgent` launches the `agy` binary and passes `configDir`/`geminiDir` as both `--gemini_dir` and `GEMINI_DIR`, matching Antigravity's `~/.gemini/antigravity-cli` config root. Current `agy` prompts use `-p`, extra directories use `--add-dir`, and native resume uses `--conversation`.
- **Non-idempotent retries.** When a `<Task>` retries, Smithers prepends a warning listing previously-called side-effect tools so the agent can verify external state before re-invoking them.

---

## SDK Agents

> Provider-backed AI SDK agent wrappers for Anthropic, OpenAI, and Hermes that work like first-class Smithers agents.

`AnthropicAgent`, `OpenAIAgent`, and `HermesAgent` are provider-backed AI SDK agents with class-style ergonomics matching the CLI agents. `AnthropicAgent` and `OpenAIAgent` wrap `ToolLoopAgent` directly; `HermesAgent` extends the OpenAI-compatible path with Hermes defaults. `ElizaAgent` adapts an Eliza runtime/character into the same Smithers agent interface when you need an in-process ElizaOS-backed agent.

<Note>API reference: Agents lists every agent class, its options, and links to source and tests.</Note>

## Import

```ts
import {
  AnthropicAgent,
  OpenAIAgent,
  createHttpTool,
  tools,
} from "smithers-orchestrator";
import { stepCountIs } from "ai";
```

## Quick Start

```ts
const claude = new AnthropicAgent({
  model: "claude-fable-5",
  tools,
  instructions: "You are a careful planner.",
  stopWhen: stepCountIs(40),
});

const codex = new OpenAIAgent({
  model: "gpt-5.6-luna",
  tools,
  instructions: "You are a precise implementation agent.",
  stopWhen: stepCountIs(40),
});
```

```tsx
import { AnthropicAgent, createSmithers } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, Task, smithers, outputs } = createSmithers({
  input: z.object({ repo: z.string() }),
  plan: z.object({
    summary: z.string(),
    risk: z.enum(["low", "medium", "high"]),
  }),
});

const claude = new AnthropicAgent({
  model: "claude-fable-5",
  instructions: "You are a careful planner. Return structured JSON.",
});

export default smithers((ctx) => (
  <Workflow name="sdk-agent-plan">
    <Task id="plan" output={outputs.plan} agent={claude}>
      {`Analyze ${ctx.input.repo} and propose a migration plan.`}
    </Task>
  </Workflow>
));
```

In workflow JSX, put the schema on the Smithers task by passing
`output={outputs.plan}`. You do not also configure the schema on the agent
constructor. At execution time the engine infers the task's Zod schema and calls
the SDK agent with `agent.generate({ outputSchema })`; the agent receives both
the prompt and the schema for that task. The `ctx` variable in the example is
the normal parameter of the `smithers((ctx) => ...)` builder, typed from the
`input` schema above.

You can also call an SDK agent once without rendering a workflow. Pass
`outputSchema` directly to `generate()` when you want typed structured output:

```ts
import { z } from "zod";

const schema = z.object({
  summary: z.string(),
  risk: z.enum(["low", "medium", "high"]),
});

const result = await claude.generate({
  prompt: "Summarize this change and classify its risk.",
  outputSchema: schema,
  timeout: { totalMs: 60_000 },
});

const parsed = schema.parse(result.output ?? JSON.parse(result.text));
```

For `AnthropicAgent`, `outputSchema` is forwarded as AI SDK
`Output.object({ schema })`, which uses Anthropic's native structured-output
request path for Claude models. `OpenAIAgent` does the same unless
`nativeStructuredOutput: false`; `HermesAgent` defaults that option to `false`
because many local Hermes servers do not honor JSON-schema response formats.

## Model Input

`AnthropicAgent` and `OpenAIAgent` accept a model ID string (`"claude-fable-5"`, `"gpt-5.6-luna"`) or a prebuilt provider model instance. `HermesAgent` accepts an optional model ID string and defaults it to `"hermes"`.

## Options

Constructors forward standard AI SDK `ToolLoopAgent` settings: `instructions`, `tools`, `stopWhen`, `maxOutputTokens`, `temperature`, `providerOptions`, `prepareCall`. The wrappers add provider model resolution on top.

- `AnthropicAgentOptions` is `ToolLoopAgentSettings` without `model`, plus required `model: string | LanguageModel`.
- `OpenAIAgentOptions` adds `nativeStructuredOutput?: boolean` and has two model forms: a string model may include `baseURL` and `apiKey`; a prebuilt OpenAI provider model must not include `baseURL` or `apiKey`.
- `HermesAgentOptions` makes `model` optional, allows `baseURL` and `apiKey`, and defaults `nativeStructuredOutput` to `false`. A runtime `baseURL` or `HERMES_BASE_URL` is required.

For the string model form of `OpenAIAgent`, pass `baseURL` and `apiKey` directly when targeting an OpenAI-compatible endpoint instead of the default OpenAI API. This is the simplest path for local servers such as llama.cpp:

```ts
const local = new OpenAIAgent({
  model: "llama-3.1-8b-instruct",
  baseURL: "http://127.0.0.1:8080/v1",
  apiKey: "none",
  tools,
  instructions: "You are a local coding assistant.",
  stopWhen: stepCountIs(40),
});
```

Set `apiKey: "none"` in the `OpenAIAgent` config when your local server accepts OpenAI-compatible requests but does not require a real key.

Some OpenAI-compatible local servers accept chat requests but do not reliably implement JSON schema structured output. For those servers, keep the output schema on the Smithers task and disable native structured output on the agent so Smithers uses prompt-based JSON extraction instead:

```ts
const local = new OpenAIAgent({
  model: "llama-3.1-8b-instruct",
  baseURL: "http://127.0.0.1:8080/v1",
  apiKey: "none",
  nativeStructuredOutput: false,
  tools,
  instructions: "You are a local coding assistant.",
  stopWhen: stepCountIs(40),
});
```

For advanced provider setup, create the AI SDK OpenAI provider yourself and pass the prebuilt model into `OpenAIAgent`:

```ts
import { createOpenAI } from "@ai-sdk/openai";

const localOpenAI = createOpenAI({
  baseURL: "http://127.0.0.1:8080/v1",
  apiKey: "none",
});

const local = new OpenAIAgent({
  model: localOpenAI("llama-3.1-8b-instruct"),
  tools,
  instructions: "You are a local coding assistant.",
  stopWhen: stepCountIs(40),
});
```

Use the `createOpenAI` path when you need provider-level configuration beyond `baseURL` and `apiKey`; in that form, `baseURL` and `apiKey` belong in the `createOpenAI` config, not in the `OpenAIAgent` constructor.

## Generic HTTP Tool

`createHttpTool()` returns an AI SDK tool that can call any REST endpoint without an OpenAPI spec. Use it as the universal escape hatch when no curated connector or MCP server exists yet.

```ts
const http = createHttpTool({
  defaultHeaders: { "user-agent": "smithers-workflow" },
});

const ops = new AnthropicAgent({
  model: "claude-fable-5",
  tools: { http },
  instructions: "Use the HTTP tool only for APIs the workflow explicitly names.",
  stopWhen: stepCountIs(20),
});
```

The tool input accepts `method`, `url`, `headers`, `query`, `body`, optional `auth` (`bearer`, `basic`, or custom header), and `timeoutMs`. Results include `ok`, `status`, `statusText`, response `headers`, and a parsed JSON or text `body`.

## Hermes

[Hermes](https://github.com/NousResearch/hermes-agent) (Nous Research) exposes an OpenAI-compatible HTTP API, so `HermesAgent` is a convenience subclass of `OpenAIAgent` that points the provider at your Hermes server and disables native structured output by default (a local Hermes server may not honor JSON-schema response formats).

```ts
import { HermesAgent } from "smithers-orchestrator";

const hermes = new HermesAgent({
  baseURL: "http://127.0.0.1:5123/v1", // or set HERMES_BASE_URL
  model: "hermes",                      // whatever model id your server advertises
  tools,
  instructions: "You are a careful implementation agent.",
  stopWhen: stepCountIs(40),
});
```

`baseURL` falls back to the `HERMES_BASE_URL` env var and must be set in either place. `apiKey` falls back to `HERMES_API_KEY` (then `"hermes"`). Pass `nativeStructuredOutput: true` if your server does honor JSON-schema output. To use Smithers *from* Hermes instead of running Hermes as a worker, see Agent Support → Hermes.

## Eliza

`ElizaAgent` wraps an in-process elizaOS `AgentRuntime` so an Eliza character and its plugins (Slack, Discord, Telegram, model providers) run as a first-class Smithers agent. Unlike the other agents on this page, it ships in a separate opt-in package that owns the `@elizaos/core` dependency, so install and import it from `@smithers-orchestrator/agent-eliza`:

```ts
import { ElizaAgent } from "@smithers-orchestrator/agent-eliza";

const eliza = new ElizaAgent({
  character: { name: "SupportBot", bio: "Answers product questions." },
  plugins: [],
  settings: { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN ?? "" },
  model: "eliza",
});
```

The harness lazily initializes the runtime on the first `generate` (or `preflight`) call and reuses it across calls. `settings` and `env` merge onto the character's own `settings` with precedence `env` over `settings` over `character.settings`. Pass an `outputSchema` on the Smithers task (or directly to `generate`) for structured output; `ElizaAgent` extracts JSON from the model's text because it does not set `supportsNativeStructuredOutput`.

## Hijack Support

SDK agents do not reopen a provider-native CLI. Smithers persists the agent conversation and reopens it through a Smithers-managed REPL via `bunx smithers-orchestrator hijack RUN_ID`.

Live-run behavior:

- Smithers captures response history after each step via `onStepEnd`.
- `bunx smithers-orchestrator hijack` waits until history is durable, aborts the current agent task (handing it off to the REPL), and opens the REPL.
- On clean REPL exit, Smithers writes updated message history back and resumes the workflow automatically.

Limits:

- Smithers reconstructs the agent from the workflow source on hijack. This means cross-engine hijack is not supported: the REPL will use the same agent class that ran originally.

## CLI vs SDK

| | CLI Agents | SDK Agents |
|---|---|---|
| Billing | Provider subscription / local CLI | API billing |
| Tools | Provider CLI tool ecosystem | Smithers tools sandbox |
| Flexibility | Native CLI flags | AI SDK `providerOptions` |
| Structured output | Prompt-injection + text JSON extraction | Native (`supportsNativeStructuredOutput`) |

**Choosing an agent for typed/JSON-output tasks.** Only `AnthropicAgent` and `OpenAIAgent` declare `supportsNativeStructuredOutput = true`, so when a `<Task>` has an `output` schema they pass it through the AI SDK's native structured-output API (`Output.object({ schema })`) and the result is schema-constrained. CLI agents (`ClaudeCodeAgent`, `CodexAgent`, etc.) do **not** set this flag: the engine falls back to injecting JSON instructions into the prompt and extracting the object from the model's text, and emits a `console.warn` ("engine ... does not support native structured output. Falling back to prompt-injection + text JSON extraction"). Schema-validation retries still run, but valid JSON shape does not guarantee meaningful values. For tasks whose primary job is producing typed/JSON output, prefer `AnthropicAgent`/`OpenAIAgent` (or, for an OpenAI-compatible endpoint that honors JSON schema, an `OpenAIAgent` with `nativeStructuredOutput` left enabled). `CodexAgent` additionally forwards the task schema to the CLI via `--output-schema` for constrained decoding, but the engine still wraps it with the prompt-injection fallback because the flag is unset.

Pass a raw `ToolLoopAgent` directly if you prefer; the wrappers are convenience, not a separate runtime.

## Transcription Tool

Use `createTranscriptionTool` when an SDK agent needs to transcribe audio as part of its tool loop. The tool accepts either an `audioUrl` or `audioBase64` input and normalizes Whisper or Deepgram responses to `{ text, provider, language?, durationSeconds? }`.

```ts
import { AnthropicAgent, createTranscriptionTool } from "@smithers-orchestrator/agents";

const transcribeAudio = createTranscriptionTool({
  provider: "deepgram", // or "whisper"
  apiKey: process.env.DEEPGRAM_API_KEY!,
});

const agent = new AnthropicAgent({
  model: "claude-fable-5",
  tools: { transcribeAudio },
  instructions: "Transcribe audio clips and summarize the result.",
});
```

## Example: Dual Setup

```ts
const useCli = process.env.USE_CLI_AGENTS === "1";

export const claude = useCli
  ? new ClaudeCodeAgent({
      model: "claude-fable-5",
      dangerouslySkipPermissions: true,
    })
  : new AnthropicAgent({
      model: "claude-fable-5",
      tools,
      instructions: "You are a careful planner.",
      stopWhen: stepCountIs(40),
    });
```


---

## MCP Toolset

> Connect a stdio MCP server to an SDK agent by projecting its tools into AI SDK tools.

`createMcpToolset` connects to a Model Context Protocol server over stdio, lists its tools once, and returns an AI SDK-compatible `tools` record for an SDK agent. This is the inbound MCP integration path: use any MCP server as an agent tool provider without writing one wrapper per external service.

For the opposite direction, where an MCP-aware client drives Smithers workflows, see MCP Server.

## Import

```ts
import { OpenAIAgent } from "smithers-orchestrator";
import {
  createMcpToolset,
  type McpServerConfig,
  type McpToolset,
  type McpToolsetOptions,
} from "@smithers-orchestrator/agents/mcp/createMcpToolset";
```

`createMcpToolset` is intentionally a deep import from `@smithers-orchestrator/agents/mcp/createMcpToolset`; it is not re-exported from the top-level `smithers-orchestrator` facade.

## Quick Start

```ts
const mcp = await createMcpToolset({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-github"],
  env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN ?? "" },
});

try {
  const agent = new OpenAIAgent({
    model: "gpt-5.6-terra",
    instructions: "Use the available tools when they are relevant.",
    tools: mcp.tools,
  });

  // Pass `agent` to a <Task>.
} finally {
  await mcp.close();
}
```

Call `close()` in a `finally` block when the workflow process owns the connection. The MCP client and spawned server process stay open until `close()` resolves.

## API

```ts
type McpServerConfig = {
  command: string;
  args?: string[];
  env?: Record<string, string>;
  cwd?: string;
};

type McpToolsetOptions = {
  include?: string[];
  exclude?: string[];
  namePrefix?: string;
  clientName?: string;
  clientVersion?: string;
};

type McpToolset = {
  tools: Record<string, import("ai").Tool>;
  toolNames: string[];
  close: () => Promise<void>;
};

declare function createMcpToolset(
  config: McpServerConfig,
  options?: McpToolsetOptions,
): Promise<McpToolset>;
```

`command`, `args`, `env`, and `cwd` are passed to the MCP SDK stdio transport. The Smithers client identity defaults to `clientName: "smithers-mcp-toolset"` and `clientVersion: "0.0.0"`.

## Tool Curation

```ts
const github = await createMcpToolset(
  {
    command: "npx",
    args: ["-y", "@modelcontextprotocol/server-github"],
    env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN ?? "" },
  },
  {
    include: ["get_issue", "create_pull_request"],
    exclude: ["create_pull_request"],
    namePrefix: "github_",
  },
);
```

`include` and `exclude` match the original MCP server tool names before `namePrefix` is applied. If both match a tool, `exclude` wins. `toolNames` contains the final names after filtering and prefixing.

## Runtime Behavior

On connect, Smithers calls `tools/list` once and wraps every selected MCP tool with AI SDK `dynamicTool`. When an agent invokes one of those tools, the wrapper calls MCP `tools/call` with the original server tool name and the model-provided arguments.

Response handling is intentionally simple:

- Successful calls return `structuredContent` when the server provides it.
- Otherwise successful calls return the joined text content blocks.
- MCP tool errors return `{ error: true, message, status: "failed" }` so the agent can inspect and recover inside its tool loop.

Schema handling follows the MCP server's advertised `inputSchema`. If a server omits it, Smithers uses an empty object JSON schema.

## CLI Agents

CLI agents consume MCP through their native configuration surfaces, not through `createMcpToolset`. Claude Code, Kimi, and Amp expose MCP config flags; Codex reads MCP servers from `~/.codex/config.toml` or `codex mcp add`.

Use `createMcpToolset` when you are building an SDK-agent workflow with `AnthropicAgent`, `OpenAIAgent`, `HermesAgent`, or a raw AI SDK tool-loop agent.


---

## Built-in Tools

> Sandboxed file and shell tools for AI agent tasks, with exact input schemas, security policies, and usage examples.

```ts
import { tools, read, write, edit, grep, bash, defineTool, getDefinedToolMetadata } from "smithers-orchestrator";
```

`tools` bundles all five tools keyed by name:

```ts
const { read, write, edit, grep, bash } = tools;
```

<Note>API reference: Tools lists every built-in tool and helper, its options, and links to source and tests.</Note>

The `smithers-orchestrator/tools` subpath also exports lower-level helpers for advanced integrations:

| Export | Purpose |
|---|---|
| `readFileTool`, `writeFileTool`, `editFileTool`, `grepTool`, `bashTool` | Call the underlying implementation directly instead of the AI SDK tool wrapper. |
| `getDefinedToolMetadata(tool)` | Read Smithers metadata (`name`, `sideEffect`, `idempotent`) from a `defineTool()` result. |
| `getToolContext()`, `runWithToolContext(ctx, fn)` | Inspect or provide the task-local tool runtime context. |
| `getToolIdempotencyKey(ctx?)`, `nextToolSeq(ctx)` | Build stable idempotency keys and task-local tool-call sequence numbers. |
| `BASH_TOOL_MAX_*` constants | Upper bounds for bash command length, args, cwd, output bytes, and timeout. |

## Sandboxing

All tools are sandboxed to `rootDir` (defaults to the workflow directory). Paths are resolved relative to this root; escapes via symlinks are rejected.

| Policy | Behavior |
|---|---|
| Path resolution | Relative paths resolve against `rootDir`. Absolute paths must fall within root. |
| Symlinks | Rejected if target is outside sandbox. |
| Output size | Process output is truncated to `maxOutputBytes` (default 200KB); `read`, `write`, and `edit` reject files, content, or patches that exceed it. |
| Timeouts | `bash` and `grep` default to 60s; exceeded processes killed with `SIGKILL`. |
| Network | `bash` blocks network commands by default. See bash. |

## Tool call state

Smithers creates the `_smithers_tool_calls` table and exposes adapter methods to insert and list rows. The current engine reads that table on retry to build warnings for previously recorded non-idempotent side-effect tool calls. The `defineTool()` wrapper itself does not insert a durable row for every call; it attaches metadata, provides `ctx.idempotencyKey`, and runs the side-effect snapshot hook when a task supplies one.

| Field | Description |
|---|---|
| `runId` | Workflow run ID |
| `nodeId` | Task node that invoked the tool |
| `iteration` | Loop iteration |
| `attempt` | Retry attempt number |
| `seq` | Sequential call counter within the task |
| `toolName` | `read`, `write`, `edit`, `grep`, or `bash` |
| `inputJson` | Serialized input arguments |
| `outputJson` | Serialized output (truncated if over limit) |
| `startedAtMs` | Start timestamp |
| `finishedAtMs` | End timestamp |
| `status` | `"success"` or `"error"` |
| `errorJson` | Error details (if `"error"`) |

## defineTool

`defineTool()` wraps custom AI SDK tools with Smithers runtime context, deterministic idempotency keys, side-effect metadata, and the side-effect snapshot hook.

```ts
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";

const placeOrder = defineTool({
  name: "wholefoods.place_order",
  description: "Place a grocery order",
  schema: z.object({
    sku: z.string(),
  }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return await wholeFoods.placeOrder({
      sku: args.sku,
      idempotencyKey: ctx.idempotencyKey,
    });
  },
});
```

- `ctx.idempotencyKey` is stable across retries and resumes for the same task iteration.
- `sideEffect: true` opts the tool into Smithers side-effect tracking.
- `idempotent: false` marks the tool for retry warnings when a previous attempt has a recorded `_smithers_tool_calls` row.
- `defineTool()` does not persist `_smithers_tool_calls` rows directly; durable rows come from runtime paths that call the Smithers DB adapter.

### Side Effects and Idempotency

Every custom tool that modifies external state **must** declare `sideEffect: true`. This is how Smithers protects your workflow during retries and resumes. Without it, Smithers treats the tool as a pure read and replays it freely, potentially sending duplicate emails, double-charging payments, or creating duplicate records.

The two flags work together:

| `sideEffect` | `idempotent` | Smithers behavior |
|---|---|---|
| `false` (default) | `true` (default) | Pure read. Safe to replay on retry. No warnings. |
| `true` | `true` | Mutates external state, but calling twice with the same input produces the same result (e.g. an upsert, a PUT request). Safe to replay. No warnings. |
| `true` | `false` | Mutates external state and is **not** safe to replay (e.g. sending an email, placing an order, charging a payment). On retry, Smithers injects a warning listing the tool as already called so the agent can verify external state before calling again. |

With `sideEffect: true` and `idempotent: false`, Smithers does two things on retry:

1. **Warns the agent.** The retry prompt lists which non-idempotent tools were already called.
2. **Provides a stable idempotency key.** `ctx.idempotencyKey` is deterministic for a given task + iteration; pass it to external APIs that support idempotency (Stripe, AWS) to deduplicate.

If your `execute` function has `sideEffect: true, idempotent: false` but omits the `ctx` parameter, Smithers logs a startup warning. This is almost always a bug: you need `ctx.idempotencyKey` to handle retries safely.

```ts
// ✗ Bad: non-idempotent side effect without ctx
const sendEmail = defineTool({
  name: "email.send",
  schema: z.object({ to: z.string(), body: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args) {  // ← missing ctx parameter, Smithers warns
    await mailer.send(args);
  },
});

// ✓ Good: uses ctx.idempotencyKey to deduplicate
const sendEmail = defineTool({
  name: "email.send",
  schema: z.object({ to: z.string(), body: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    await mailer.send({ ...args, idempotencyKey: ctx.idempotencyKey });
  },
});
```

### What counts as a side effect

The rule is simple: **if you cannot undo it with `git reset`, mark it as a side effect.**

A side effect is any mutation the runtime should not blindly repeat on retry. If a custom tool talks to an external API, writes to a database, sends a message, or triggers a webhook, mark it.

The built-in `write` and `edit` tools are registered as `sideEffect: true` and `idempotent: false` because their file mutations are not safe to blindly replay on retry; like `bash`, they are treated conservatively. All three built-in mutating tools (`write`, `edit`, `bash`) are side-effecting.

| Tool | Side effect? | Why |
|---|---|---|
| Built-in `read`, `grep` | No | Pure reads |
| Built-in `write`, `edit` | **Yes** | Sandboxed file writes are tracked by git, but replaying on retry could overwrite content that diverged since the first call |
| Built-in `bash` | **Yes** | Arbitrary shell commands may not be safe to repeat |
| Custom tool calling an external API | **Yes** | Mutates state outside the sandbox |
| Custom tool writing to a database | **Yes** | External persistent state |
| Custom tool sending a Slack message | **Yes** | Irreversible external communication |
| Custom tool creating a GitHub PR | **Yes** | External state visible to others |

---

## read

Read a file from the sandbox.

```ts
{ path: string }  // relative to rootDir or absolute
```

Returns file contents as UTF-8. Throws `"File too large"` if size exceeds `maxOutputBytes`.

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { read, grep } from "smithers-orchestrator";

const codeAgent = new Agent({
  model: openai("gpt-5.6-sol"),
  tools: { read, grep },
});
```

```tsx
{/* outputs comes from createSmithers() */}
<Task id="review" output={outputs.review} agent={codeAgent}>
  Read the file src/auth.ts and identify any security vulnerabilities.
</Task>
```

---

## write

Write content to a file. Creates parent directories as needed.

```ts
{
  path: string      // relative to rootDir or absolute
  content: string
}
```

Returns `"ok"`. Throws `"Content too large"` if content exceeds `maxOutputBytes`. Logs content hash (SHA-256) and byte size; full content is not stored.

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { write, read } from "smithers-orchestrator";

const writerAgent = new Agent({
  model: openai("gpt-5.6-luna"),
  tools: { write, read },
});
```

---

## edit

Apply a unified diff patch to an existing file.

```ts
{
  path: string    // file to patch
  patch: string   // unified diff format
}
```

Returns `"ok"`. The file must exist. Reads current contents, applies the patch via `applyPatch`, writes back. Throws on size limits (`"Patch too large"`, `"File too large"`) or mismatched context (`"Failed to apply patch"`). Logs patch hash and byte size.

```
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -10,3 +10,4 @@
   const token = jwt.sign(payload, secret);
+  logger.info("Token issued", { userId: payload.sub });
   return token;
```

---

## grep

Search for a regex pattern using `ripgrep`.

```ts
{
  pattern: string    // regex
  path?: string      // directory or file (default: rootDir)
}
```

Returns matching lines with file paths and line numbers (`rg -n` format). Exit code 1 (no matches) returns empty string. Exit code 2 throws stderr as error. Requires `ripgrep` in PATH.

```
src/auth.ts:15:  if (token.expired()) {
src/auth.ts:42:  validateToken(token);
tests/auth.test.ts:8:  const token = createTestToken();
```

---

## bash

Run an executable directly with arguments.

```ts
{
  cmd: string                     // executable path/name; no shell parsing
  args?: string[]                 // arguments
  opts?: { cwd?: string }        // working directory (sandboxed)
}
```

Use `args` for arguments. If you need shell syntax such as pipes or redirects, invoke a shell explicitly, for example `cmd: "sh", args: ["-lc", "..."]`.

Returns combined stdout and stderr. Working directory defaults to `rootDir`. Timeout: 60s (killed with `SIGKILL` via process group). Non-zero exit codes throw.

### Network Blocking

Controlled by `allowNetwork` in `RunOptions`, `--allow-network` on CLI, or server config. Default: blocked.

When blocked, Smithers tokenizes `cmd` plus `args`. Executable basenames are
matched for known network tools, URL tokens are blocked by prefix, and `git`
plus a remote-operation token is blocked.

| Category | Match |
|---|---|
| HTTP clients | executable basename `curl` or `wget` |
| URL tokens | any token starting with `http://` or `https://` |
| Package managers | executable basename `npm`, `bun`, or `pip` |
| Git remote ops | `git` plus a `push`, `pull`, `fetch`, `clone`, or `remote` token |

Local git commands (`git status`, `git diff`, `git log`) are allowed.

<Warning>
**Enforcement vs. bypassable denylist.** True OS-level network isolation is
enforced **only on macOS**, where a blocked `bash` runs under `sandbox-exec`
with a network-deny profile (when `sandbox-exec` is available). On Linux and
every other platform, including the environments Smithers runs in for CI,
cloud, and production, `allowNetwork:false` cannot enforce a kernel sandbox and
degrades to the token/basename denylist in the table above. That denylist is
best-effort defense-in-depth, **not** a security boundary: it is trivially
bypassable by a shell, an interpreter (`python -c` assembling a URL from parts,
bash `/dev/tcp`), or a renamed binary. When isolation is unenforced, Smithers
logs a `TOOL_NETWORK_ISOLATION_UNENFORCED` observability warning. **Do not rely
on `allowNetwork:false` to sandbox untrusted code**. Run untrusted workloads
under a real `<Sandbox>` provider with egress controls
instead.
</Warning>

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { bash } from "smithers-orchestrator";

const devAgent = new Agent({
  model: openai("gpt-5.6-terra"),
  tools: { bash },
});
```

```tsx
{/* outputs comes from createSmithers() */}
<Task id="lint" output={outputs.lint} agent={devAgent}>
  Run the linter on src/ and report any issues.
</Task>
```

---

## Using Tools with Agents

Pass tools to an AI SDK agent and assign the agent to a `<Task>`:

```tsx
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { createSmithers, read, write, edit, grep, bash, Task } from "smithers-orchestrator";
import { z } from "zod";

const codeAgent = new Agent({
  model: openai("gpt-5.6-luna"),
  tools: { read, write, edit, grep, bash },
  instructions: "You are a senior engineer. Use the available tools to complete tasks.",
});

const { Workflow, smithers, outputs } = createSmithers({
  result: z.object({ summary: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="refactor">
    <Task id="refactor" output={outputs.result} agent={codeAgent}>
      {`Refactor the function in ${ctx.input.file} to improve readability.`}
    </Task>
  </Workflow>
));
```

The full bundle works too:

```ts
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { tools } from "smithers-orchestrator";

const fullAgent = new Agent({
  model: openai("gpt-5.6-terra"),
  tools,
});
```

## Configuration

| Option | Default | Description |
|---|---|---|
| `rootDir` | Workflow directory | Sandbox root |
| `allowNetwork` | `false` | Allow network commands in `bash` |
| `maxOutputBytes` | `200000` (200KB) | Max output size per tool |
| `toolTimeoutMs` | `60000` (60s) | Timeout for `bash` and `grep` |

```ts
import { Effect } from "effect";
import { runWorkflow } from "smithers-orchestrator";

const result = await Effect.runPromise(runWorkflow(workflow, {
  input: { file: "src/auth.ts" },
  rootDir: "/home/project",
  allowNetwork: false,
  maxOutputBytes: 500_000,
  toolTimeoutMs: 120_000,
}));
```


---

## Common External Tools

> Patterns for hitting GitHub, Linear, Notion, Slack, Obsidian as tools.

<Note>API reference: Tools lists every built-in tool and helper, its options, and links to source and tests.</Note>

For each external service, you have three choices:

1. **OpenAPI tools**: point `createOpenApiTools()` at the service's OpenAPI spec. See OpenAPI tools.
2. **CLI in a task**: if the service has a CLI (`gh`, `linear`, `notion`, `slack`), run it inside a `<Task>` via the `bash` tool. See side-effect tools with idempotency.
3. **Custom `defineTool`**: wrap the service's REST API in a Zod-validated tool. See `defineTool`.

### Example: CLI in a task (Pattern 2)

```tsx
<Task
  id="open-pr"
  tools={[bash]}
  prompt="Run: gh pr create --title 'Fix login bug' --body 'Closes #42'"
/>
```

Run `gh auth login` once on the host before executing. The `bash` tool executes in the task's sandbox, so credentials set in the environment carry through automatically.

## Quick decision

| Service | Recommended approach |
|---|---|
| GitHub      | `gh` CLI in a task (auth via `gh auth login`) |
| Linear      | `linear` CLI in a task, or OpenAPI tools |
| Notion      | OpenAPI tools (Notion publishes a spec) |
| Slack       | OpenAPI tools or `slack` CLI |
| Obsidian    | `bash` tool with vault path; no API needed |

Always mark side-effecting tools with `sideEffect: true` and use `ctx.idempotencyKey` so retries don't double-fire.

---

## Community Connectors

> Package long-tail integrations as manifest-declared tools, triggers, and auth requirements.

Community connectors are package-level integration descriptors. They let a maintainer publish one connector package that projects agent-callable tools and triggers onto Smithers' existing Tier 0 substrate instead of adding one workflow node per app.

A connector package is declarative at the boundary: the manifest says what tool and trigger surfaces exist, which auth grants they require, and which runtime adapter should load them. Runtime code may still live in the package, but Smithers only discovers it through the loader contract below.

## Package Layout

```text
smithers-connector-acme/
  package.json
  smithers.connector.json
  openapi/acme.yaml
  mcp/server.json
  src/index.ts
  README.md
```

`smithers.connector.json` is the contract. `package.json` should expose the loader module with a normal ESM export and include a `smithers.connector` field that points at the manifest:

```json
{
  "name": "smithers-connector-acme",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "smithers": {
    "connector": "./smithers.connector.json"
  }
}
```

## Manifest Format

The manifest format id is `smithers.connector.v1`. Unknown top-level keys are ignored, but unknown keys inside `tools`, `triggers`, `auth`, and `surfaces` must fail validation so package authors notice typos before an agent receives the tool catalog.

```json
{
  "schema": "smithers.connector.v1",
  "id": "acme",
  "name": "Acme",
  "version": "0.1.0",
  "description": "Curated Acme tools for Smithers agents.",
  "loader": "./src/index.ts",
  "auth": {
    "oauth": {
      "provider": "acme",
      "grant": "authorization_code_pkce",
      "scopes": ["tickets:read", "tickets:write"],
      "tenantKey": "workspace_id"
    }
  },
  "surfaces": {
    "openapi": {
      "spec": "./openapi/acme.yaml",
      "baseUrl": "https://api.acme.example"
    },
    "mcp": {
      "command": "node",
      "args": ["./dist/mcp-server.js"]
    },
    "webhooks": {
      "provider": "acme",
      "signature": "hmac-sha256",
      "idempotencyKey": "$.event_id"
    }
  },
  "tools": [
    {
      "name": "acme_create_ticket",
      "surface": "openapi",
      "operationId": "createTicket",
      "description": "Create one Acme ticket after checking for an existing ticket with the same external id.",
      "auth": "oauth",
      "scopes": ["tickets:write"],
      "sideEffect": true,
      "idempotency": {
        "key": "externalId",
        "required": true
      }
    },
    {
      "name": "acme_search_tickets",
      "surface": "mcp",
      "tool": "search_tickets",
      "description": "Search Acme tickets by text, status, assignee, or customer.",
      "auth": "oauth",
      "scopes": ["tickets:read"],
      "sideEffect": false
    }
  ],
  "triggers": [
    {
      "name": "acme_ticket_updated",
      "surface": "webhook",
      "event": "ticket.updated",
      "auth": "oauth",
      "scopes": ["tickets:read"],
      "dedupe": {
        "key": "$.event_id"
      },
      "payloadSchema": {
        "type": "object",
        "required": ["ticketId"],
        "properties": {
          "ticketId": { "type": "string" }
        }
      }
    }
  ],
  "tokenBroker": {
    "audience": "acme",
    "defaultTtlSeconds": 300,
    "allowedActions": ["acme_create_ticket", "acme_search_tickets"]
  }
}
```

## Loader Contract

A Smithers connector loader must:

1. validate the manifest before loading package code.
2. resolve auth through the OAuth plane and token broker, never by handing durable credentials to an LLM-visible tool call.
3. project tools from `surfaces.openapi`, `surfaces.mcp`, or the package loader into AI SDK-compatible tools.
4. register triggers from `surfaces.webhooks` with signature verification and dedupe before workflow dispatch.
5. enforce scopes for every tool and trigger invocation against the connected user and tenant.
6. Preserve idempotency metadata so retries and resumes can avoid duplicate write-side effects.

Loader modules export one named `loadConnector` function:

```ts
export type SmithersConnectorContext = {
  manifest: unknown;
  connection: {
    userId: string;
    tenantId?: string;
    scopes: string[];
  };
  tokenBroker: {
    issue(input: {
      audience: string;
      scopes: string[];
      action: string;
      ttlSeconds?: number;
    }): Promise<{ token: string; expiresAt: string }>;
  };
};

export async function loadConnector(ctx: SmithersConnectorContext) {
  return {
    tools: {},
    triggers: [],
  };
}
```

The loader may add custom tools that cannot be represented by OpenAPI or MCP, but those tools still need manifest entries. The manifest remains the auditable catalog Smithers can show to agents and humans.

## Tool Declarations

Each tool declaration describes one agent-callable capability, not one raw endpoint. Prefer a small curated surface with clear names and task-shaped descriptions.

Required fields:

| Field | Purpose |
|---|---|
| `name` | Stable tool name exposed to the agent. Prefix with the connector id. |
| `surface` | `openapi`, `mcp`, or `custom`. |
| `description` | Agent-facing behavior, limits, and recovery guidance. |
| `auth` | Auth profile key, or `none` for public tools. |
| `scopes` | Minimum delegated scopes required for this action. |
| `sideEffect` | `true` for writes, sends, charges, deletes, or external state changes. |

OpenAPI tools bind with `operationId`. MCP tools bind with the upstream MCP `tool` name. Custom tools bind to code returned by `loadConnector`.

Side-effecting tools must declare `idempotency.required: true` unless the upstream provider supplies an equivalent idempotency key, request id, or natural unique key.

## Trigger Declarations

Triggers describe external events that can start or resume workflows. They are not workflow nodes and should not grow into a provider-specific palette.

Required fields:

| Field | Purpose |
|---|---|
| `name` | Stable trigger id exposed to workflow authors. |
| `surface` | `webhook`, `poll`, or `mcp`. |
| `event` | Provider event name or polling resource. |
| `auth` | Auth profile key required to subscribe or poll. |
| `dedupe.key` | JSONPath or provider event id used for idempotent dispatch. |

Webhook triggers must declare the provider signature scheme and payload mapper through `surfaces.webhooks`. Poll triggers must declare interval bounds and cursor storage requirements in the manifest before registration.

## Auth Requirements

Connector auth declarations are requirements, not secrets. The OAuth plane owns authorization-code + PKCE flows, encrypted refresh-token storage, single-flight refresh, and per-user/per-tenant scoping.

Supported auth profiles:

| Profile | Use |
|---|---|
| `oauth` | Delegated per-user OAuth through the Smithers OAuth plane. |
| `apiKey` | User-supplied key stored in the credential vault, exposed only through the token broker. |
| `none` | Public or local-only tools. |

Do not put tokens, client secrets, refresh tokens, service-account credentials, or shared bot tokens in a connector package. Runtime calls receive scoped action tokens from the `tokenBroker`, and the broker exchanges or unwraps provider credentials outside the agent transcript.

## Tier 0 Integration Points

Community connectors plug into existing universal surfaces:

| Tier 0 surface | Connector field | Loader behavior |
|---|---|---|
| OAuth plane | `auth.oauth` | Resolve delegated user consent, tenant key, scopes, and refresh lifecycle. |
| Scoped token broker | `tokenBroker` and per-tool `scopes` | Issue short-lived, revocable, auditable action tokens the LLM never sees. |
| OpenAPI tools | `surfaces.openapi` + `tools[].operationId` | Build curated tools with `createOpenApiTools`; never dump every endpoint by default. |
| MCP client | `surfaces.mcp` + `tools[].tool` | Pass selected MCP tools through with names and descriptions from the manifest. |
| Webhook ingress | `surfaces.webhooks` + `triggers[]` | Verify signatures, map payloads, dedupe events, and dispatch workflows. |
| Generic HTTP / sandbox | `surface: "custom"` | Run package code for cases that cannot be expressed as OpenAPI or MCP. |

## Anti-Patterns

- Do not add a node per app. Connectors contribute agent tools and triggers, not a giant fixed-schema workflow palette.
- Do not publish every OpenAPI operation as a tool. Collapse noisy endpoint sets into task-shaped actions.
- Do not bypass delegated OAuth with service-account or shared-bot tokens.
- Do not refresh tokens independently in each tool call; refresh must go through the single-flight OAuth/token broker path.
- Do not omit idempotency from write tools or webhook triggers.
- Do not hide tool behavior in package code when it can be declared in the manifest.

## Review Checklist

Before accepting a community connector, verify:

- The manifest validates as `smithers.connector.v1`.
- Every tool has a clear description, auth profile, scope list, and side-effect marker.
- Every write tool has idempotency metadata.
- Every trigger has signature verification or polling cursor rules plus dedupe.
- OAuth scopes are the minimum needed for the declared tools and triggers.
- The package uses OpenAPI, MCP, webhook ingress, or the token broker instead of inventing a parallel integration plane.

---

## Ecosystem

> Community projects built on Smithers.

## Burns

Workspace-first local control plane for Smithers. Single UI for authoring, running, and supervising workflows across repositories. Register repos, launch runs, stream events, inspect frames, handle approvals.

- React web app, ElectroBun (a Bun-native Electron alternative) desktop shell, or headless CLI
- AI-assisted workflow authoring via local agent CLIs
- SQLite-backed workspace registry

<Card title="Burns" icon="github" href="https://github.com/l3wi/burns">
  github.com/l3wi/burns
</Card>

## Ralphinho

Multi-agent development workflows. Two independent workflows:

- **Ralphinho** (scheduled-work) -- decomposes RFC into work units, runs tier-based quality pipelines (implement, test, review), lands via merge queue with CI verification.
- **Improvinho** (review-discovery) -- three parallel discovery lenses (refactoring, type safety, architecture), deduplicates findings. Optionally pushes to Linear.

Requires Bun and Jujutsu (`jj`). Supports Claude and Codex agents.

<Card title="Ralphinho" icon="github" href="https://github.com/enitrat/ralphinho">
  github.com/enitrat/ralphinho
</Card>

## Cairo Coder

AI-powered Cairo smart contract generator. RAG pipeline (DSPy) converting natural language to Cairo contracts for Starknet. Uses Smithers with Claude and Codex agents.

<Card title="Cairo Coder" icon="github" href="https://github.com/KasarLabs/cairo-coder">
  github.com/KasarLabs/cairo-coder
</Card>

## Agentix

Opinionated RFC-to-production orchestrator. Multi-phase pipelines: research, plan, implement, test, review. Role-based agents, conflict-aware merge queues, security/performance gates. DDD + BDD + TDD by default.

<Card title="Agentix" icon="github" href="https://github.com/AbdelStark/agentix">
  github.com/AbdelStark/agentix
</Card>

## Era

Generic multi-phase development workflow engine. Research, Plan, Implementation, Testing, Review, Fix, Final Review pipeline with outer Loop. Role-based agents, intelligent caching, dual-layer prompts.

<Card title="Era" icon="github" href="https://github.com/ClementWalter/era">
  github.com/ClementWalter/era
</Card>

## Local Isolated Ralph

Kubernetes-native Smithers workflow runner. Runs workflows as isolated K8s Jobs and CronJobs via k3s/k3d. Sandboxed container execution.

<Card title="Local Isolated Ralph" icon="github" href="https://github.com/SamuelLHuber/local-isolated-ralph">
  github.com/SamuelLHuber/local-isolated-ralph
</Card>

## Publishing Workflow Packs

Use the Workflow Catalog metadata when publishing a pack or success story: workflow IDs, required agents, Gateway scopes, inputs, outputs, write behavior, sandbox runtime, and validation command. That keeps community projects installable by operators who did not author the underlying TSX.

---

## PI Integration

> Use PI as a Smithers workflow CLI backend and understand how PI extensibility composes with Smithers declarative orchestration.

Smithers provides deterministic orchestration (workflow graph, approvals, retries, durable state). PI provides adaptive agent capabilities (providers, models, extensions, skills, prompt templates). Use both when you need deterministic execution with flexible agent behavior.

## Integration Modes

### 1) PI as Workflow Agent

```tsx
import { PiAgent } from "smithers-orchestrator";

const pi = new PiAgent({
  provider: "openai",
  model: "gpt-5.6-luna",
  mode: "text",
});

{/* outputs comes from createSmithers() */}
<Task id="implementation" output={outputs.implementation} agent={pi}>
  {`Implement feature X and explain tradeoffs.`}
</Task>
```

`PiAgent` supports all PI CLI flags: provider/model, tools, extensions, skills, prompt templates, themes, sessions. Text mode uses `--print` by default; JSON/RPC modes set `--mode` and omit `--print`.

PI sessions are first-class hijack targets. `bunx smithers-orchestrator hijack RUN_ID --target pi` reopens the PI session for local steering.

### 2) PI Server Client

Drive Smithers server APIs from a PI extension or Bun process via `@smithers-orchestrator/pi-plugin`:

```ts
import { runWorkflow, approve, streamEvents } from "@smithers-orchestrator/pi-plugin";
```

`@smithers-orchestrator/pi-plugin` currently publishes TypeScript source entrypoints; run it with Bun or bundle it before using it from plain Node.

The older `smithers-orchestrator/pi-plugin` and `smithers-orchestrator/pi-extension` subpaths were removed; use the scoped package directly.

### 3) Hybrid: PI Extensibility + Smithers Orchestration

- Keep orchestration in Smithers (`<Sequence>`, `<Parallel>`, `<Branch>`, `<Loop>`).
- Run adaptive logic in PI tasks (extensions/skills/provider overrides).

Patterns:

1. PI skill-driven coding task inside a Smithers `<Task>`.
2. PI extension command that starts/resumes Smithers workflows via server API or `@smithers-orchestrator/pi-plugin`.
3. Smithers workflow output persisted to SQLite and consumed by later PI-assisted tasks.

## Hijacking PI Sessions

PI is a native-session hijack backend.

- Live run: Smithers watches PI's event stream, waits between blocking tool calls, then hands off the session.
- Finished/cancelled run: Smithers reopens the latest persisted PI session.
- Relaunch uses the stored session ID: `pi --session <id>`.
- Clean exit resumes the workflow automatically.

Session persistence:

- `PiAgent` defaults `noSession` to `true` for one-shot calls.
- For workflow hijack/resume/streaming, Smithers keeps session persistence enabled automatically.
- `mode: "json"` is not required for hijack support.

## Setup

1. Install PI CLI and add to `PATH`.
2. Configure PI credentials via env/config (prefer over CLI args for API keys).
3. Instantiate `PiAgent` with explicit options in workflows.
4. For server-driven workflows, use `@smithers-orchestrator/pi-plugin`.

```bash
# Verify PI is installed
pi --version
```

## Design Guidance

| Use `PiAgent` tasks when | Use Smithers-native tasks when |
|---|---|
| You need PI capabilities inside deterministic workflows | You need strict reproducibility and narrow tool contracts |
| You want PI calls as auditable workflow steps | |

## Limitations

Smithers does not provide a chat interface for PI. Chat UI integration is the responsibility of the host application using `@smithers-orchestrator/pi-plugin`.

---

## Hermes & Eliza

> First-class Smithers support for the Hermes agent (a native Hermes plugin) and the Eliza (elizaOS) agent (built in by default). Make Smithers the durable control plane for both.

Smithers ships **first-class support for two agent runtimes**:
Hermes (Nous Research) via a
native Hermes plugin, and [Eliza (elizaOS)](https://github.com/elizaOS/eliza),
which has Smithers built in by default. In both, Smithers becomes the durable
control plane: the agent launches crash-safe, multi-step workflows, clears human
approval gates, and stays aware of every in-flight run.

The relationship is bidirectional:

- **Hermes / Eliza drive Smithers**: they run and operate durable workflows.
- **Smithers drives Hermes**: `HermesCliAgent` lets a workflow `<Task>` delegate
  to the Hermes agent itself (see CLI agents).

## Hermes: a native plugin (not just MCP)

Smithers installs a **native Hermes plugin**, which is far richer than a bare MCP
entry. MCP can only expose tools. The plugin adds:

- **Slash commands** (`/smithers run|ps|inspect|output|approve|deny|watch`) usable in the
  Hermes CLI and every gateway (Discord, Telegram, Slack).
- **A live-run status injector** (`pre_llm_call` hook) so Hermes is aware of
  active and paused runs, and any pending approval gates, on every turn.
- **Tools**: `smithers_run`, `smithers_ps`, `smithers_inspect`, `smithers_approve`,
  `smithers_deny`, `smithers_output`, `smithers_human_answer`.
- **A bundled skill** (`smithers:orchestrate`) that teaches Hermes to prefer a
  durable workflow over a one-off skill.
- **Gateway push-back**: a detached run launched from a chat session reports its
  result back into that same session.
- **Slack approval buttons**: an approval gate becomes Approve / Deny buttons that
  map straight to `bunx smithers-orchestrator approve` / `bunx smithers-orchestrator deny`.

### Distribution is one command

The plugin ships inside the Smithers CLI, so installing it is one command:

```bash
bunx smithers-orchestrator hermes

# `hermes` is a convenience alias for the generic form:
bunx smithers-orchestrator mcp add --agent hermes
```

The command writes the plugin into `~/.hermes/plugins/smithers/`, installs the
gateway hook into `~/.hermes/hooks/smithers/`, enables it in
`~/.hermes/config.yaml`, and also registers the MCP server as a floor (so tools
work even if user plugins are disabled). It is idempotent: rerun it any time to
upgrade.

Verify it loaded:

```bash
hermes plugins list        # shows `smithers`
hermes -z "/smithers ps"   # lists your runs from inside Hermes
```

## Eliza (elizaOS): built in by default

Eliza has Smithers support **built in by
default**: there is no plugin to install. Any Eliza agent can launch and operate
durable Smithers workflows out of the box, and a provider injects live Smithers
run status into the agent's context on every turn.

What the agent gets:

- `RUN_SMITHERS_WORKFLOW`: start a durable workflow for any multi-step or
  background task (falls back to `create-workflow` to author one from the
  request).
- `SMITHERS_APPROVE` / `SMITHERS_DENY`: clear approval gates.
- `SMITHERS_RUNS` provider: every turn, the agent automatically sees what is
  running and what needs a human.

It reaches Smithers through the `smithers` CLI (override the binary with
`$SMITHERS_BIN`).

## Why a workflow, not a skill

In both runtimes the guidance is the same: a Smithers workflow is a **superset of
a skill**. A skill is static instructions; a workflow is executable, durable,
typed, inspectable, composable, and optimizable. Capture reusable procedures as
workflows (even small one-task ones), and optimize them like skills with
`bunx smithers-orchestrator eval`, scorers, and `bunx smithers-orchestrator optimize`. See the
[smithers skill](https://github.com/smithersai/smithers/blob/main/skills/smithers/SKILL.md).

## Links

- Hermes agent: github.com/NousResearch/hermes-agent
- Eliza (elizaOS): github.com/elizaOS/eliza
- Hermes plugin source: [apps/cli/src/hermes-plugin](https://github.com/smithersai/smithers/tree/main/apps/cli/src/hermes-plugin)

---

## Daytona Sandbox Provider

> Run a Smithers <Sandbox> child workflow inside a Daytona sandbox with createDaytonaSandboxProvider.


`@smithers-orchestrator/daytona` is a first-class Smithers `SandboxProvider`
backed by the [Daytona](https://www.daytona.io/) SDK. It runs a `<Sandbox>`
child workflow's request inside a Daytona sandbox: it uploads the request JSON,
runs the entry command, and reads the result JSON back. The shared provider-kit
owns the request/result protocol, egress, secret scrubbing, and cleanup, so this
package only maps the small `SandboxSession` seam onto Daytona.

The provider id is `daytona-sandbox` (exported as `DAYTONA_SANDBOX_PROVIDER_ID`).

```ts
import {
  createDaytonaSandboxProvider,
  registerDaytonaSandboxProvider,
  DAYTONA_SANDBOX_PROVIDER_ID,
} from "smithers-orchestrator/daytona";
```

## Credentials

`@daytonaio/sdk` is an optional dependency, imported lazily inside the session.
Install it to use the real provider:

```
npm install @daytonaio/sdk
```

Credentials come from factory options first, then the Daytona env chain. They
are used only to build the local SDK client. They are never written into the
request JSON and never forwarded into the remote sandbox env.

- `DAYTONA_API_KEY`: API key (required for a real client).
- `DAYTONA_API_URL`: API base url (optional).
- `DAYTONA_TARGET`: target region (optional).

The same values may be passed as `apiKey`, `apiUrl`, and `target` factory
options, or through `clientOptions`.

## Usage

Pass the provider straight to `<Sandbox>`:

```tsx
import { createSmithers, Sandbox } from "smithers-orchestrator";
import { createDaytonaSandboxProvider } from "smithers-orchestrator/daytona";

const provider = createDaytonaSandboxProvider({
  image: "ubuntu:22.04",
  autoStopInterval: 15, // minutes idle before Daytona auto-stops
  ephemeral: true,      // delete the sandbox on disconnect
});

export default parent.smithers((ctx) => (
  <parent.Workflow name="daytona-sandbox-run">
    <Sandbox
      id="remote-edit"
      provider={provider}
      workflow={childWorkflow}
      input={{ prompt: ctx.input.prompt }}
      output={parent.outputs.result}
    />
  </parent.Workflow>
));
```

Or register it once and reference it by id:

```ts
import { registerDaytonaSandboxProvider } from "smithers-orchestrator/daytona";

const unregister = registerDaytonaSandboxProvider({ image: "ubuntu:22.04" });

// <Sandbox provider="daytona-sandbox" workflow={child} output={outputs.result} />
```

## Request/result contract

The kit writes the request JSON to `.smithers/sandbox-request.json` in the
workdir and hands the entry command two env vars:

- `SMITHERS_SANDBOX_REQUEST_PATH`: where to read the request JSON.
- `SMITHERS_SANDBOX_RESULT_PATH`: where to write the result JSON.

The entry command either prints the result JSON to stdout or writes it to
`SMITHERS_SANDBOX_RESULT_PATH`. The result is `{ bundlePath }` or a structured
`{ status, output|outputs, patches?, diffBundle?, runId? }`. The kit fills
`remoteRunId`/`workspaceId` from the Daytona sandbox id when the entry omits
them.

## Factory options

- `image` or `snapshot`: the base for `daytona.create` (pass one, not both).
- `autoStopInterval`: minutes idle before Daytona auto-stops (default `15`).
- `ephemeral`: delete the sandbox on disconnect (default `true`).
- `resources`, `labels`, `env`: forwarded to `daytona.create`.
- `command`: entry command (default `node /workspace/run-smithers-sandbox.js`).
- `workdir`: default `/workspace`.
- `cleanup`: `"destroy"` (default) or `"keep"`.
- `client` / `clientOptions`: inject an SDK client or its constructor options.

Per-run `request.config` may carry `image`, `snapshot`, `resources`, `labels`,
or a `workspace` block (`snapshotId`, `idleTimeoutSecs`,
`persistence: "ephemeral"`) which maps onto the create options.

## SDK subset used

`daytona.create({ image?|snapshot?, envVars, labels, ephemeral, autoStopInterval,
resources })`, `sandbox.fs.uploadFile(Buffer, path)`,
`sandbox.fs.downloadFile(path)`, `sandbox.process.executeCommand(command, cwd,
env, timeoutSecs)` (Daytona merges stderr into `result`), and
`daytona.delete(sandbox)`.

## Cleanup and cost

`cleanup: "destroy"` (default) deletes the sandbox after the run. `cleanup:
"keep"` leaves it running. `ephemeral` plus `autoStopInterval` bound idle cost
even when cleanup is skipped, so a crashed orchestrator does not leave a sandbox
billing forever. You pay for Daytona sandbox time from create to teardown.

`createMockDaytonaSandboxEnvironment(handler, faults?)` is an in-memory SDK
double for tests. It needs zero credentials, so unit tests run in CI without
touching Daytona.

---

## Vercel Sandbox Provider

> Run a Smithers <Sandbox> child workflow on Vercel Sandbox compute with createVercelSandboxProvider.


`@smithers-orchestrator/vercel` is a first-class Smithers `SandboxProvider`
backed by the [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) SDK. It
runs a `<Sandbox>` child workflow's request on Vercel Sandbox compute: it ships
the request JSON into the sandbox workdir, runs the entry command, and reads the
result JSON back. The shared provider-kit owns request shipping, egress, result
parsing, secret scrubbing, and cleanup, so this package only supplies the Vercel
`createSession` seam.

The provider id is `vercel-sandbox` (exported as `VERCEL_SANDBOX_PROVIDER_ID`).
The default workdir is `/vercel/sandbox`.

```ts
import {
  createVercelSandboxProvider,
  registerVercelSandboxProvider,
  VERCEL_SANDBOX_PROVIDER_ID,
} from "smithers-orchestrator/vercel";
```

## Credentials

`@vercel/sandbox` is an optional dependency, imported lazily inside the session.
Install it to use the real provider:

```bash
npm install @vercel/sandbox
```

OIDC is preferred; the access-token trio is the fallback. Auth is resolved and
validated before the SDK is touched, so a misconfigured provider fails fast with
`INVALID_INPUT`. These values are used only to create the sandbox. They are
never written into the request JSON and never forwarded into the remote command
env.

- `VERCEL_OIDC_TOKEN` (preferred), or
- `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID`.

Each can also be passed as a factory option (`oidcToken`, `token`, `teamId`,
`projectId`).

## Usage

```tsx
import { createSmithers, Sandbox } from "smithers-orchestrator";
import { createVercelSandboxProvider } from "smithers-orchestrator/vercel";

const provider = createVercelSandboxProvider({
  runtime: "node24",
  vcpus: 2,
  ports: [3000],
  maxDurationMs: 45 * 60_000,
});

export default parent.smithers((ctx) => (
  <parent.Workflow name="vercel-sandbox-run">
    <Sandbox
      id="remote-build"
      provider={provider}
      workflow={childWorkflow}
      input={{ prompt: ctx.input.prompt }}
      output={parent.outputs.result}
    />
  </parent.Workflow>
));
```

Or register it once and reference it by id:

```ts
import { registerVercelSandboxProvider } from "smithers-orchestrator/vercel";

const unregister = registerVercelSandboxProvider({ runtime: "node24" });

// <Sandbox provider="vercel-sandbox" workflow={child} output={outputs.result} />
```

## Request/result contract

The kit writes the request JSON to `.smithers/sandbox-request.json` in the
workdir and hands the entry command two env vars:

- `SMITHERS_SANDBOX_REQUEST_PATH`: where to read the request JSON.
- `SMITHERS_SANDBOX_RESULT_PATH`: where to write the result JSON.

The entry command either prints the result JSON to stdout or writes it to
`SMITHERS_SANDBOX_RESULT_PATH`. The result is `{ bundlePath }` or a structured
`{ status, output|outputs, patches?, diffBundle?, runId? }`. When ports are
declared, the reachable `sandbox.domain(port)` is surfaced through a heartbeat.

## Factory options

- `runtime`: Vercel Sandbox runtime (default `node24`).
- `vcpus`: vCPU count passed to `resources`.
- `ports`: ports whose domains are surfaced.
- `timeoutMs` / `maxDurationMs`: see duration cap below.
- `persist`: `stop()` (pause) instead of `delete()` on teardown.
- `command`: entry command.
- `workdir`: default `/vercel/sandbox`.
- `cleanup`: `"destroy"` (default) or `"keep"`.
- `env`: merged into the command env.
- `client` / `createOptions`: inject the SDK class or extra `Sandbox.create`
  options.

## Duration and plan cap

The default session timeout is 5 minutes. `options.timeoutMs` (or
`request.toolTimeoutMs`) maps to the create `timeout`. Durations above the
5-minute default warn through a heartbeat and call `sandbox.extendTimeout()`, up
to `options.maxDurationMs` (default 45 minutes). A request above that cap throws
`INVALID_INPUT` rather than provisioning a sandbox that would overrun the plan.
Raise `maxDurationMs` for Pro (up to 5 hours).

## Cleanup and cost

Ephemeral sandboxes are deleted permanently on teardown (`cleanup: "destroy"`,
the default). Set `persist: true` to `stop()` (pause) the sandbox instead so it
can be resumed. `cleanup: "keep"` skips teardown entirely. You pay for Vercel
Sandbox wall-clock from create to teardown.

`createMockVercelSandboxEnvironment(handler, config?)` is an in-memory SDK
double for tests. Pass it as `options.client`; it needs zero credentials, so
unit tests run in CI without touching Vercel.

---

## AWS Sandbox Provider

> Run a Smithers <Sandbox> child workflow on AWS Fargate or CodeBuild with createAwsSandboxProvider, transporting the bundle through S3.


`@smithers-orchestrator/aws` is a first-class Smithers `SandboxProvider` that
runs a `<Sandbox>` child workflow's request on AWS. It has two modes: Fargate
(ECS `RunTask`, the default) and CodeBuild. AWS gives the orchestrator no shared
filesystem with the remote task, so the request/result bundle is transported
through an S3 bucket you already own. The shared provider-kit owns the
request/result protocol, egress, secret scrubbing, and cleanup; this package
supplies the S3 transport plus the ECS/CodeBuild runners.

The provider id is `aws-sandbox` (exported as `AWS_SANDBOX_PROVIDER_ID`).

```ts
import {
  createAwsSandboxProvider,
  registerAwsSandboxProvider,
  AWS_SANDBOX_PROVIDER_ID,
} from "smithers-orchestrator/aws";
```

## Credentials

Authentication uses the standard AWS SDK v3 credential chain: env vars, shared
config, SSO, or an instance/task role. No explicit credentials are passed to the
factory and none are ever forwarded into the remote task env. The remote task
authenticates with its own task or build IAM role.

The `@aws-sdk/*` clients are optional dependencies, imported lazily. Install the
ones your mode needs:

- `@aws-sdk/client-s3` (always).
- `@aws-sdk/client-ecs` (Fargate).
- `@aws-sdk/client-codebuild` (CodeBuild).
- `@aws-sdk/client-cloudwatch-logs` (only with `captureLogs`).

## Prerequisites

The provider does not provision infrastructure. Set these up first:

- An S3 `bucket` that already exists. The provider only manages the key prefix
  `smithers/sandbox/<runId>/<sandboxId>/` under it.
- For Fargate: an ECS `cluster`, a registered `taskDefinition`, at least one VPC
  `subnet`, optional `securityGroups`, and the `containerName` inside the task
  definition. The task role needs S3 read/write on the prefix.
- For CodeBuild: a CodeBuild `projectName`. The build role needs S3 read/write
  on the prefix.

## Usage

```tsx
import { createAwsSandboxProvider } from "smithers-orchestrator/aws";

// Fargate (default)
const provider = createAwsSandboxProvider({
  region: "us-east-1",
  bucket: "my-smithers-sandbox-bucket", // must already exist
  cluster: "smithers",
  taskDefinition: "smithers-sandbox:7",
  subnets: ["subnet-abc123"],
  securityGroups: ["sg-def456"],
  assignPublicIp: "ENABLED",
  containerName: "runner",
});

// <Sandbox provider={provider} workflow={child} output={outputs.result} />
```

```ts
// CodeBuild
const codebuild = createAwsSandboxProvider({
  mode: "codebuild",
  region: "us-east-1",
  bucket: "my-smithers-sandbox-bucket",
  projectName: "smithers-sandbox",
});
```

Or register either provider once and reference it by id with
`registerAwsSandboxProvider(...)` then `<Sandbox provider="aws-sandbox" />`.

## Request/result contract

The kit hands the entry command `SMITHERS_SANDBOX_REQUEST_PATH` and
`SMITHERS_SANDBOX_RESULT_PATH`. Because there is no shared filesystem, the
container round-trips those files through S3. The provider injects four more
vars so the entry can find them:

- `SMITHERS_SANDBOX_S3_BUCKET`: the transport bucket.
- `SMITHERS_SANDBOX_S3_PREFIX`: `smithers/sandbox/<runId>/<sandboxId>`.
- `SMITHERS_SANDBOX_REQUEST_S3_KEY`: S3 key of the request JSON.
- `SMITHERS_SANDBOX_RESULT_S3_KEY`: S3 key the entry must write the result to.

Every workdir path maps to `s3://<bucket>/<prefix>/<runId>/<sandboxId>/<encodeURIComponent(workdir-relative path)>` so two files that share a basename never collide. The entry
either prints the result JSON to stdout or writes it to the result key. An infra
failure (task or build fails with no result written) throws. A result file with
`status: "failed"` is a normal failed bundle. Fargate reports a real numeric
container exit code; CodeBuild reports a status (`SUCCEEDED` maps to 0, else 1).

## Factory options

- `mode`: `"fargate"` (default) or `"codebuild"`.
- `region`, `bucket`: required in both modes.
- Fargate: `cluster`, `taskDefinition`, `subnets`, `containerName` (required),
  `securityGroups`, `assignPublicIp`, `logGroupName`.
- CodeBuild: `projectName` (required).
- `captureLogs`: pull CloudWatch logs, truncated to `maxOutputBytes`.
- `command`, `workdir` (default `/workspace`), `env`.
- `cleanup`: `"destroy"` (default) or `"keep"`.
- `clients` / `client` / `clientOptions`: inject SDK doubles or client options.

## Cleanup and cost

`cleanup: "destroy"` (default) stops the task or build if it is still running
and deletes the transient S3 objects. `cleanup: "keep"` leaves them. You pay for
Fargate task time or CodeBuild build minutes, plus S3 storage of the small
transient bundle objects. Set a short lifecycle TTL on the `smithers/sandbox/`
prefix to reap anything a crash leaves behind. EC2 mode is documented future
work.

`createMockAwsSandboxEnvironment(handler, mockOptions?)` provides in-memory S3,
ECS, CodeBuild, and CloudWatch Logs doubles for tests. It needs zero AWS
credentials, so unit tests run in CI without touching AWS.

---

## GCP Sandbox Provider

> Run a Smithers <Sandbox> child workflow on Cloud Run Jobs with createGcpSandboxProvider, transporting the bundle through Cloud Storage.


`@smithers-orchestrator/gcp` is a first-class Smithers `SandboxProvider` for
Google Cloud. It executes a `<Sandbox>` child workflow's request on Cloud Run
Jobs and ships the request/result bundle through a Cloud Storage bucket, because
Cloud Run Jobs give no shared filesystem back to the caller. The shared
provider-kit owns the request/result protocol, egress, secret scrubbing, and
cleanup; this package supplies the GCS transport plus the Cloud Run runner.

The provider id is `gcp-sandbox` (exported as `GCP_SANDBOX_PROVIDER_ID`).

```ts
import {
  createGcpSandboxProvider,
  registerGcpSandboxProvider,
  GCP_SANDBOX_PROVIDER_ID,
} from "smithers-orchestrator/gcp";
```

## Credentials

Authentication is Application Default Credentials:
`GOOGLE_APPLICATION_CREDENTIALS` (a service-account key file) or workload
identity, plus `GOOGLE_CLOUD_PROJECT`. Local credentials are never forwarded
into the container. The container env is `options.env` plus the Smithers, egress,
and GCS-transport variables.

`@google-cloud/run` and `@google-cloud/storage` are optional dependencies,
imported lazily inside the session, so the package loads without them. Install
both to use the real provider:

```
npm install @google-cloud/run @google-cloud/storage
```

## Prerequisites

The provider does not create the bucket. Set these up first:

- A Cloud Storage `bucket` that already exists (the transport). Set a short
  lifecycle TTL on the `smithers/sandbox/` prefix to reap crash leftovers.
- A Cloud Run `jobName` to run, or set `createJob` to have a per-run Job created
  and torn down. When `createJob` is set you must also pass a container `image`
  (Cloud Run requires one to create the Job). The job's service account needs
  read/write on the bucket prefix.

## Required options

`createGcpSandboxProvider({ projectId, location, bucket, jobName })`: all four
are required and missing ones throw `INVALID_INPUT`. `projectId` falls back to
`GOOGLE_CLOUD_PROJECT`.

| option      | meaning                                                     |
| ----------- | ---------------------------------------------------------- |
| `projectId` | GCP project id                                             |
| `location`  | Cloud Run region, e.g. `us-central1`                       |
| `bucket`    | a pre-existing Cloud Storage bucket (transport)            |
| `jobName`   | Cloud Run Job to run (or created per-run when `createJob`) |

Other knobs: `prefix` (default `smithers/sandbox`), `command`, `workdir`
(default `/workspace`), `env`, `cleanup` (`"destroy"` or `"keep"`),
`timeoutSec`, `createJob`, `sandboxId(request)`, and
`client`/`clients`/`clientOptions` for SDK injection.

## Usage

```tsx
import { Sandbox } from "smithers-orchestrator";
import { createGcpSandboxProvider } from "smithers-orchestrator/gcp";

const provider = createGcpSandboxProvider({
  projectId: "my-project",
  location: "us-central1",
  bucket: "my-smithers-sandbox",
  jobName: "smithers-sandbox-runner",
});

// <Sandbox provider={provider} workflow={child} output={outputs.result} />
```

Or register it once and reference it by id with
`registerGcpSandboxProvider(...)` then `<Sandbox provider="gcp-sandbox" />`.

## Request/result contract

The kit writes `.smithers/sandbox-request.json` and expects
`.smithers/sandbox-result.json` back, handing the container
`SMITHERS_SANDBOX_REQUEST_PATH` and `SMITHERS_SANDBOX_RESULT_PATH`. Because there
is no shared filesystem, the container round-trips those files through GCS. The
runner injects four extra vars:

- `SMITHERS_SANDBOX_GCS_BUCKET`: the transport bucket.
- `SMITHERS_SANDBOX_GCS_PREFIX`: object-name prefix.
- `SMITHERS_SANDBOX_REQUEST_GCS_OBJECT`: object holding the request JSON.
- `SMITHERS_SANDBOX_RESULT_GCS_OBJECT`: object the entry must write result JSON to.

Every workdir-relative path maps to
`<prefix>/<runId>/<sandboxId>/<percent-encoded workdir-relative path>` (the full path, not just the basename, so same-named files never collide). Cloud Run reports task counts
and conditions, not a numeric exit code: a succeeded task is exit 0 and a failed
task is exit 1. An infra failure (execution failed, no result written) throws. A
run that writes `status: "failed"` result JSON is a normal failed bundle the kit
materializes.

## Cleanup and cost

`cleanup: "destroy"` (default) deletes the transient GCS objects and, when
`createJob` made a per-run job, deletes that job. `cleanup: "keep"` leaves
everything. The bucket itself is never created or deleted. You pay for Cloud Run
Job execution time plus GCS storage of the small transient objects. Compute
Engine execution is documented future work.

`createMockGcpSandboxEnvironment(handler, config?)` provides in-memory Cloud
Storage and Cloud Run doubles for tests. It needs zero real GCP credentials or
bucket, so unit tests run in CI without touching GCP.

---

## Serverless deployment

> How to run the Smithers control plane serverlessly across platforms -- pick a host, a storage descriptor, and (for full-OS agents) a sandbox provider. Includes the per-platform matrix and cost drivers.


Deploying Smithers serverlessly is three independent choices. This page maps
each choice onto the platforms Smithers supports today, states honestly what
runs where, and points at the exact exports. For the conceptual model behind it
(in-process vs full-OS agents, what's billed), read
Where agents run, sandboxes & cost first.

1. **A control-plane host** -- where the engine drives the run.
2. **A storage descriptor** -- where durable run state lives.
3. **A sandbox provider** -- *only* if your workflow uses CLI / full-OS agents
   via `<Sandbox>`; in-process SDK agents need none.

## 1. Control-plane host

The engine renders the workflow, schedules tasks, and persists frames. Where it
can run depends on the runtime's system access:

| Host | Runtime | Runs the engine today? |
|---|---|---|
| A container running **Bun** (ECS, App Runner, Cloud Run service, GKE, a VM) | Bun | ✅ Yes |
| The **Node** runtime (Vercel Node function, AWS Lambda Node, a Node server) | Node (has `fs` + `child_process`) | ⚠️ In progress |
| Cloudflare **Worker** / Vercel **Edge** | V8 isolate (no `fs`/`child_process`) | ⚠️ In progress |

<Warning>
  **The engine currently requires the Bun runtime.** Run it on any container
  platform (ECS, App Runner, Cloud Run, GKE, a VM) under Bun and the full engine
  works today. Two portability efforts are in flight:

  - **Node runtime** (Vercel Node functions, Lambda Node): closest, since Node
    has `fs` + `child_process`. The engine module now **imports cleanly under
    plain Node** (all `bun:sqlite`-reaching value imports are lazy-loaded, guarded
    by a node-subprocess regression test), and it no longer hard-binds the Bun
    platform layer (`RunOptions.effectPlatformLayer` accepts `NodeContext.layer`,
    supplied by your entrypoint from `@effect/platform-node`). The engine now
    **drives a run to completion under plain Node**, proven end-to-end by a
    node-subprocess regression test that runs a compute-task workflow against
    PGlite and asserts the run reaches `finished`; the worker-task dispatch path
    no longer requires bun-sqlite (under Node the `@effect/cluster` single-runner
    uses the in-memory MessageStorage driver, matching the non-durable `:memory:`
    sqlite the Bun path already used). Still to land before calling Node fully
    supported: validating agent (CLI/SDK) tasks and the gateway under Node.
  - **Isolates** (Workers, Edge): further out -- the engine and gateway also use
    `node:fs` / `node:child_process` (git/jj worktrees on a real filesystem),
    which an `@effect/platform` `FileSystem`/`Command` seam must abstract. Today
    an isolate can host **storage + in-process SDK agents** (via
    `createSmithersCloudflare`), but not the run driver.
</Warning>

## 2. Storage descriptor

Durable run state is dialect-abstracted (`sqlite | postgres`), so the same
control plane runs on any of these:

| Descriptor | Use for |
|---|---|
| `bun:sqlite` (default) | Local dev on Bun. |
| PGlite | Embedded Postgres, zero external DB. |
| Postgres | Production: RDS, Cloud SQL, Neon, Supabase, any Postgres. Use the pooled/serverless endpoint on functions. |
| `createCloudflareDurableObjectSqliteDescriptor` | Cloudflare Workers durable run-of-record (real interactive transactions). |
| `createCloudflareD1SqliteDescriptor` | Cloudflare D1 -- **read-mostly only** (no interactive transactions; unsafe for durable run state). |

On Node hosts, point Smithers at Postgres and you are done. On Cloudflare, build
the API with `createSmithersCloudflare` over the Durable Object SQLite
descriptor (see Cloudflare).

## 3. Sandbox provider (only for full-OS / CLI agents)

In-process SDK agents (`AnthropicAgent`, `OpenAIAgent`, `HermesAgent`,
`ElizaAgent`) need **no** provider -- they are HTTPS calls in your process. CLI
agents (`ClaudeCodeAgent`, `CodexAgent`, `OpenCodeAgent`, …) spawn a vendor
binary and must run inside a container, which a `SandboxProvider` supplies at
each `<Sandbox>` boundary. Each provider ships a `create…SandboxProvider` and a
`createMock…SandboxEnvironment` test double; AWS/GCP/Vercel/Daytona also ship a
`register…SandboxProvider` helper (on Cloudflare the provider is wired through
`createSmithersCloudflare`).

| Platform | Package | Create fn · provider id | Execution · transport |
|---|---|---|---|
| Cloudflare | `@smithers-orchestrator/cloudflare` | `createCloudflareSandboxProvider` · `cloudflare-sandbox` | `@cloudflare/sandbox` container |
| AWS | `@smithers-orchestrator/aws` | `createAwsSandboxProvider` · `aws-sandbox` | Fargate (ECS) or CodeBuild runner · S3 bundle transport |
| GCP | `@smithers-orchestrator/gcp` | `createGcpSandboxProvider` · `gcp-sandbox` | Cloud Run Jobs runner · GCS bundle transport |
| Vercel | `@smithers-orchestrator/vercel` | `createVercelSandboxProvider` · `vercel-sandbox` | Vercel Sandbox compute |
| Daytona | `@smithers-orchestrator/daytona` | `createDaytonaSandboxProvider` · `daytona-sandbox` | Daytona sandboxes |
| Any host (Docker) | `@smithers-orchestrator/sandbox` | Docker `http-runner` | Local/remote Docker |

<Note>
  There is no dedicated Kubernetes provider yet. On a cluster, run the
  control plane as a normal Node deployment and use the Docker `http-runner`
  (or register a custom `SandboxProvider` that submits a K8s Job) for the
  full-OS agent path.
</Note>

## Per-platform recipes

The control plane runs under **Bun** on any of these container platforms today;
the Node-runtime and isolate paths are in progress (see the host section above).

| Platform | Control plane (Bun container) | Storage | Sandbox provider (CLI agents) | Status |
|---|---|---|---|---|
| **Local / any VM** | Bun | `bun:sqlite` / PGlite / Postgres | Docker `http-runner` | ✅ Full |
| **AWS** | Bun on ECS / App Runner | Postgres (RDS) | `createAwsSandboxProvider` (Fargate/CodeBuild) | ✅ Full |
| **GCP** | Bun on Cloud Run / GKE | Postgres (Cloud SQL) | `createGcpSandboxProvider` (Cloud Run Jobs) | ✅ Full |
| **Kubernetes** | Bun deployment | Postgres | Docker `http-runner` / custom | ✅ Full (no first-class provider yet) |
| **Vercel** | Node function | Postgres (Neon pooled) | `createVercelSandboxProvider` | ⚠️ Node-runtime engine load in progress |
| **Cloudflare** | Worker + Durable Object | DO-SQLite | `createCloudflareSandboxProvider` | ⚠️ Storage + SDK agents; run driver in progress |

## What it costs

Three independent axes (detailed in
the execution model):

- **Model tokens / subscription** -- billed by the model provider.
- **Control-plane host compute** -- your Node function/server wall-clock (or
  Worker/DO requests + duration on Cloudflare).
- **Sandbox provider compute** -- only when a `<Sandbox>` runs: the container/VM
  wall-clock from create → run → teardown, plus the bundle-transport storage
  (S3/GCS/DO). A workflow of only in-process SDK agents pays this axis nothing.

## Read next

- Where agents run, sandboxes & cost -- the model.
- Cloudflare -- Workers, DO-SQLite, and the provider.
- AWS · GCP · Vercel · Daytona sandbox providers.
- Control plane · Production hardening.

---

## Run on Plue

> Execute a Smithers workflow on Plue workspaces through the Plue sandbox provider.


`run-on-plue` executes a Smithers workflow script on Plue infrastructure instead of the local machine. It uses the `<Sandbox>` provider seam with `createPlueSandboxProvider`, and the provider shells through the `plue` CLI so Plue owns workspace lifecycle, authentication, quotas, and Freestyle VM access.

Use it when a child workflow needs a real Plue workspace: a repo-bound VM with SSH, `git`, `jj`, Node, network egress, and bootstrapped Claude Code and Codex CLIs.

## What it does

The workflow lives at `.smithers/workflows/run-on-plue.tsx`. It accepts a path to any Smithers `.tsx` workflow, reads that file, and sends it to a Plue-backed sandbox provider.

The provider:

1. Creates a Plue workspace with `plue workspace create --repo <owner>/<repo>`.
2. Polls `plue workspace view --format json` until the workspace is running and has an SSH command.
3. Connects over SSH in batch mode.
4. Installs Bun if missing, then installs `@anthropic-ai/claude-code` and `@openai/codex` when those CLIs are absent.
5. Seeds Claude and Codex auth via `plue workspace exec --seed-agent-auth claude,codex`.
6. Ships a small remote project containing the child workflow source, `agents.ts`, `package.json`, and `input.json`.
7. Runs `smithers-orchestrator up` in the workspace and reads the remote run result back over SSH.
8. Deletes the workspace unless `keepWorkspace` is true.

The parent workflow receives:

```ts
{
  status: "finished" | "failed" | "cancelled";
  output?: unknown;
  remoteRunId?: string;
  workspaceId?: string;
}
```

## Requirements

- `plue` CLI installed and authenticated on the host running Smithers. Verify with `plue auth status`.
- The target Plue server must have workspaces enabled. Local Plue uses `SMITHERS_FEATURE_FLAGS_WORKSPACES=true`.
- The Plue server must be configured for real Freestyle workspaces, including the Freestyle API key required by that server (`SMITHERS_FREESTYLE_API_KEY` or the environment wiring used by your Plue deployment).
- The repo passed as `repo` must exist in Plue and be accessible to the authenticated user.
- Claude auth must be available locally. The preferred path is Claude Code subscription OAuth in the OS credential store; `ANTHROPIC_API_KEY` only works when that key has usable API billing.
- Codex auth must be available locally. The preferred path is `~/.codex/auth.json`; `OPENAI_API_KEY` is only a fallback for setups where the Codex CLI honors API-key auth.
- If `plue` is not on `PATH`, set `PLUE_BIN` or pass `plueBin` in the workflow input.

Secrets are seeded at run time by the Plue CLI and written into the VM with restrictive file permissions. Do not bake Claude, Codex, OpenAI, Anthropic, or Freestyle credentials into workspace snapshots or committed files.

## Run a workflow remotely

Run the included demo child workflow:

```bash
bunx smithers-orchestrator up .smithers/workflows/run-on-plue.tsx --input '{
  "script": ".smithers/workflows/plue-demo-child.tsx",
  "repo": "alice/smoke-test"
}'
```

Pass input through to the remote child:

```bash
bunx smithers-orchestrator up .smithers/workflows/run-on-plue.tsx --input '{
  "script": "workflows/review-child.tsx",
  "repo": "acme/app",
  "input": {
    "ticketId": "APP-123"
  }
}'
```

Use a local or custom CLI binary and keep the workspace for inspection:

```bash
bunx smithers-orchestrator up .smithers/workflows/run-on-plue.tsx --input '{
  "script": ".smithers/workflows/plue-demo-child.tsx",
  "repo": "alice/smoke-test",
  "plueBin": "/path/to/plue",
  "keepWorkspace": true
}'
```

## Use the provider directly

`run-on-plue` is the default wrapper, but workflows can pass the provider object to `<Sandbox>` directly:

```tsx
import { createSmithers } from "smithers-orchestrator";
import { Sandbox } from "@smithers-orchestrator/components";
import { createPlueSandboxProvider } from "../lib/plue-provider";
import { childWorkflow } from "./child";
import { z } from "zod/v4";

const { Workflow, smithers, outputs } = createSmithers({
  result: z.object({ summary: z.string() }),
});

const provider = createPlueSandboxProvider({
  repo: "acme/app",
  keepWorkspace: false,
});

export default smithers((ctx) => (
  <Workflow name="remote-child">
    <Sandbox
      id="plue-run"
      provider={provider}
      workflow={childWorkflow}
      input={{ prompt: ctx.input.prompt }}
      output={outputs.result}
      reviewDiffs={false}
      timeoutMs={30 * 60_000}
    />
  </Workflow>
));
```

## Non-interactive caveat

Keep `reviewDiffs={false}` for Plue runs. `<Sandbox>` defaults to fail-closed diff review, which expects a local review/approval interaction before applying file changes. The v1 Plue provider is non-interactive: it runs the remote child, maps the remote result into the parent run, and does not open a human approval loop inside the VM.

Design child workflows so they return structured output or artifacts. If you need to inspect the remote VM after a failure, run with `keepWorkspace: true`, then use `plue workspace view` or `plue workspace ssh` to connect.

## Claude and Codex bootstrap

Remote workspaces may not have all agent CLIs preinstalled. The provider's bootstrap is idempotent:

- `bun` is installed from the official installer when missing.
- `claude` is installed with `bun i -g @anthropic-ai/claude-code` when missing.
- `codex` is installed with `bun i -g @openai/codex` when missing.
- `~/.bun/bin` is prepended to `PATH` for remote commands.
- `plue workspace exec --seed-agent-auth claude,codex` stages Claude Code and Codex credentials before the child workflow runs.

The remote mini-project includes an `agents.ts` that exports both `ClaudeCodeAgent` and `CodexAgent`, so child workflows can import or define tasks that use either CLI once bootstrap completes.


===============================================================================

# Smithers Events

> Smithers event surface: how to subscribe, the event categories, and the full SmithersEvent discriminated union.

---

## Events

> Subscribe to lifecycle events. Full event union lives in Types.

`SmithersEvent` is the discriminated union of lifecycle events the runtime and observability layer understand. Most variants are emitted by the runtime; reserved variants are called out in Event Types. The full type definition lives in Types; that's the source of truth for field shapes.

## Subscribe via `onProgress`

```ts
import { runWorkflow } from "smithers-orchestrator";
import { Effect } from "effect";
import workflow from "./workflow";

await Effect.runPromise(runWorkflow(workflow, {
  input: { task: "fix bug" },
  onProgress: (event) => {
    if (event.type === "NodeStarted")  console.log(`▶ ${event.nodeId} (attempt ${event.attempt})`);
    if (event.type === "NodeFinished") console.log(`✓ ${event.nodeId}`);
    if (event.type === "NodeFailed")   console.error(`✗ ${event.nodeId}`, event.error);
  },
}));
```

## Read from the NDJSON log

Events append to `.smithers/executions/<runId>/logs/stream.ndjson` (configure with `logDir` / `--log-dir`; disable with `--no-log`).

```bash
tail -f .smithers/executions/<runId>/logs/stream.ndjson | jq .
jq 'select(.type == "NodeFailed")' .smithers/executions/<runId>/logs/stream.ndjson
jq -r .type .smithers/executions/<runId>/logs/stream.ndjson | sort | uniq -c | sort -rn
```

Or with the CLI:

```bash
bunx smithers-orchestrator events RUN_ID --json
bunx smithers-orchestrator events RUN_ID --type tool-call --node analyze
```

## Common fields

```ts
type CommonFields    = { type: string; runId: string; timestampMs: number };
type NodeScoped      = CommonFields & { nodeId: string; iteration: number };
type AttemptScoped   = NodeScoped   & { attempt: number };
```

Every event includes `type`, `runId`, `timestampMs`. Node-scoped events add `nodeId` and `iteration`. Attempt-scoped add `attempt`.

## Event categories

Used by `bunx smithers-orchestrator events --type <category>` and the metrics layer.

| Category | Events |
|---|---|
| `run` | RunAutoResumed, RunAutoResumeSkipped, RunStarted, RunStatusChanged, RunStateChanged, RunFinished, RunFailed, RunCancelled, RunContinuedAsNew, RunHijackRequested, RunHijacked, RetryTaskStarted, RetryTaskFinished, RunForked, ReplayStarted |
| `frame` | FrameCommitted |
| `node` | NodePending, NodeStarted, TaskHeartbeat, TaskHeartbeatTimeout, NodeFinished, NodeFailed, NodeCancelled, NodeSkipped, NodeRetrying, NodeWaitingApproval, NodeWaitingTimer |
| `approval` | ApprovalRequested, ApprovalGranted, ApprovalAutoApproved, ApprovalDenied |
| `tool-call` | ToolCallStarted, ToolCallFinished |
| `agent` | AgentEvent, AgentTraceEvent, AgentTraceSummary, AgentSessionEvent |
| `output` | NodeOutput |
| `revert` | RevertStarted, RevertFinished, TimeTravelStarted, TimeTravelFinished, TimeTravelJumped |
| `workflow` | WorkflowReloadDetected, WorkflowReloaded, WorkflowReloadFailed, WorkflowReloadUnsafe |
| `scorer` | ScorerStarted, ScorerFinished, ScorerFailed |
| `token` | TokenUsageReported |
| `timer` | TimerCreated, TimerFired, TimerCancelled |
| `memory` | MemoryFactSet, MemoryRecalled, MemoryMessageSaved |
| `openapi` | OpenApiToolCalled |
| `sandbox` | SandboxCreated, SandboxShipped, SandboxHeartbeat, SandboxBundleReceived, SandboxCompleted, SandboxFailed, SandboxDiffReviewRequested, SandboxDiffAccepted, SandboxDiffRejected |
| `snapshot` | SnapshotCaptured |
| `supervisor` | SupervisorStarted, SupervisorPollCompleted |

`RunStateChanged` is categorized as `run` because the type is part of the public event union, but the current runtime does not emit it. Use `RunStatusChanged` from the stream plus `getRun` / `computeRunState` for derived run state.

`OpenApiToolCalled` is categorized as `openapi` for forward compatibility, but the current OpenAPI tool factory records Effect metrics and log spans rather than emitting that event onto the run event bus.

## Built-in metrics

| Event | Metric |
|---|---|
| `RunStarted` | `smithers.runs.total` |
| `NodeStarted` | `smithers.nodes.started` |
| `NodeFinished` | `smithers.nodes.finished` |
| `NodeFailed` | `smithers.nodes.failed` |
| `ApprovalGranted` / `ApprovalDenied` | Approval counters |
| `TokenUsageReported` | Token usage counters per model/agent |

`trackSmithersEvent` from `smithers-orchestrator/observability` exposes this mapping for custom integrations. See Observability for the full OTLP/Prometheus setup.

---

## Event Types

> Current SmithersEvent variants, event categories, and top-level fields.

`SmithersEvent` is the discriminated union understood by the runtime and observability layer. Runtime-emitted variants are persisted to the event log, passed to `runWorkflow({ onProgress })`, streamed by Gateway, and filtered by `bunx smithers-orchestrator events --type <category>`. Reserved variants are documented here when they are typed but not currently emitted.

Every variant has `type`, `runId`, and `timestampMs`. Node-scoped variants add `nodeId` and `iteration`. Attempt-scoped variants add `attempt`.

For subscription examples, CLI usage, and built-in metrics, see Events.

## Status Types

```ts
type RunStatus =
  | "running"
  | "waiting-approval"
  | "waiting-event"
  | "waiting-timer"
  | "waiting-quota"
  | "paused"
  | "finished"
  | "continued"
  | "failed"
  | "cancelled";

type RunState =
  | "running"
  | "waiting-approval"
  | "waiting-event"
  | "waiting-timer"
  | "waiting-quota"
  | "paused"
  | "recovering"
  | "stale"
  | "orphaned"
  | "failed"
  | "cancelled"
  | "succeeded"
  | "unknown";
```

## Event Variants

Discriminate on the `type` field, e.g. `event.type === "NodeFailed"`, to handle specific lifecycle moments.

<Warning>
**In a browser UI the shape is different.** `useGatewayRunEvents` (and the vanilla
`streamRunEventsResilient`) yield **`GatewayEventFrame`** objects, not raw
`SmithersEvent`s. On a frame the event NAME is `frame.event` and every other field
lives under `frame.payload`. So the client-side equivalents are:

```tsx
const { events } = useGatewayRunEvents(runId, { afterSeq: 0 });
for (const frame of events) {
  if (frame.event === "NodeFailed") {
    const { nodeId, error } = frame.payload;   // NOT frame.type / frame.nodeId
    // error is the errorToJson value: read error.message
  }
}
```

Use `frame.event === "NodeStarted" | "NodeFinished" | "NodeFailed"` to build a
per-node status view, and `frame.payload.nodeId` to key it. The table below lists
each variant's fields; in a UI those fields are on `frame.payload`.
</Warning>

| Event | Top-level fields |
|---|---|
| `SupervisorStarted` | `runId`, `pollIntervalMs`, `staleThresholdMs`, `timestampMs` |
| `SupervisorPollCompleted` | `runId`, `staleCount`, `resumedCount`, `skippedCount`, `durationMs`, `timestampMs` |
| `RunAutoResumed` | `runId`, `lastHeartbeatAtMs`, `staleDurationMs`, `timestampMs` |
| `RunAutoResumeSkipped` | `runId`, `reason`, `timestampMs` |
| `RunStarted` | `runId`, `timestampMs` |
| `RunStatusChanged` | `runId`, `status`, `timestampMs` |
| `RunStateChanged` | `runId`, `before`, `after`, `timestampMs` |
| `RunFinished` | `runId`, `timestampMs` |
| `RunFailed` | `runId`, `error`, `timestampMs` |
| `RunCancelled` | `runId`, `timestampMs` |
| `RunContinuedAsNew` | `runId`, `newRunId`, `iteration`, `carriedStateSize`, `ancestryDepth?`, `timestampMs` |
| `RunHijackRequested` | `runId`, `target?`, `timestampMs` |
| `RunHijacked` | `runId`, `nodeId`, `iteration`, `attempt`, `engine`, `mode`, `resume?`, `cwd`, `timestampMs` |
| `SandboxCreated` | `runId`, `sandboxId`, `runtime`, `configJson`, `timestampMs` |
| `SandboxShipped` | `runId`, `sandboxId`, `runtime`, `bundleSizeBytes`, `timestampMs` |
| `SandboxHeartbeat` | `runId`, `sandboxId`, `remoteRunId?`, `progress?`, `timestampMs` |
| `SandboxBundleReceived` | `runId`, `sandboxId`, `bundleSizeBytes`, `patchCount`, `hasOutputs`, `timestampMs` |
| `SandboxCompleted` | `runId`, `sandboxId`, `remoteRunId?`, `runtime`, `status`, `durationMs`, `timestampMs` |
| `SandboxFailed` | `runId`, `sandboxId`, `runtime`, `error`, `timestampMs` |
| `SandboxDiffReviewRequested` | `runId`, `sandboxId`, `patchCount`, `totalDiffLines`, `timestampMs` |
| `SandboxDiffAccepted` | `runId`, `sandboxId`, `patchCount`, `timestampMs` |
| `SandboxDiffRejected` | `runId`, `sandboxId`, `reason?`, `timestampMs` |
| `FrameCommitted` | `runId`, `frameNo`, `xmlHash`, `timestampMs` |
| `NodePending` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `NodeStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `timestampMs` |
| `TaskHeartbeat` | `runId`, `nodeId`, `iteration`, `attempt`, `hasData`, `dataSizeBytes`, `intervalMs?`, `timestampMs` |
| `TaskHeartbeatTimeout` | `runId`, `nodeId`, `iteration`, `attempt`, `lastHeartbeatAtMs`, `timeoutMs`, `timestampMs` |
| `NodeFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `timestampMs` |
| `NodeFailed` | `runId`, `nodeId`, `iteration`, `attempt`, `error`, `timestampMs` |
| `NodeCancelled` | `runId`, `nodeId`, `iteration`, `attempt?`, `reason?`, `timestampMs` |
| `NodeSkipped` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `NodeRetrying` | `runId`, `nodeId`, `iteration`, `attempt`, `timestampMs` |
| `NodeWaitingApproval` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `NodeWaitingTimer` | `runId`, `nodeId`, `iteration`, `firesAtMs`, `timestampMs` |
| `ApprovalRequested` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ApprovalGranted` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ApprovalAutoApproved` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ApprovalDenied` | `runId`, `nodeId`, `iteration`, `timestampMs` |
| `ToolCallStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `toolCallId`, `toolName`, `seq`, `timestampMs` |
| `ToolCallFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `toolCallId`, `toolName`, `seq`, `status`, `timestampMs` |
| `NodeOutput` | `runId`, `nodeId`, `iteration`, `attempt`, `text`, `stream`, `timestampMs` |
| `AgentEvent` | `runId`, `nodeId`, `iteration`, `attempt`, `engine`, `event`, `timestampMs` |
| `RetryTaskStarted` | `runId`, `nodeId`, `iteration`, `resetDependents`, `resetNodes`, `timestampMs` |
| `RetryTaskFinished` | `runId`, `nodeId`, `iteration`, `resetNodes`, `success`, `error?`, `timestampMs` |
| `RevertStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer`, `timestampMs` |
| `RevertFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer`, `success`, `error?`, `timestampMs` |
| `TimeTravelStarted` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer?`, `timestampMs` |
| `TimeTravelFinished` | `runId`, `nodeId`, `iteration`, `attempt`, `jjPointer?`, `success`, `vcsRestored`, `resetNodes`, `error?`, `timestampMs` |
| `TimeTravelJumped` | `runId`, `fromFrameNo`, `toFrameNo`, `timestampMs`, `caller?` |
| `WorkflowReloadDetected` | `runId`, `changedFiles`, `timestampMs` |
| `WorkflowReloaded` | `runId`, `generation`, `changedFiles`, `timestampMs` |
| `WorkflowReloadFailed` | `runId`, `error`, `changedFiles`, `timestampMs` |
| `WorkflowReloadUnsafe` | `runId`, `reason`, `changedFiles`, `timestampMs` |
| `ScorerStarted` | `runId`, `nodeId`, `scorerId`, `scorerName`, `timestampMs` |
| `ScorerFinished` | `runId`, `nodeId`, `scorerId`, `scorerName`, `score`, `timestampMs` |
| `ScorerFailed` | `runId`, `nodeId`, `scorerId`, `scorerName`, `error`, `timestampMs` |
| `TokenUsageReported` | `runId`, `nodeId`, `iteration`, `attempt`, `model`, `agent`, `inputTokens`, `outputTokens`, `cacheReadTokens?`, `cacheWriteTokens?`, `reasoningTokens?`, `timestampMs` |
| `SnapshotCaptured` | `runId`, `frameNo`, `contentHash`, `timestampMs` |
| `RunForked` | `runId`, `parentRunId`, `parentFrameNo`, `branchLabel?`, `timestampMs` |
| `ReplayStarted` | `runId`, `parentRunId`, `parentFrameNo`, `restoreVcs`, `timestampMs` |
| `MemoryFactSet` | `runId`, `namespace`, `key`, `timestampMs` |
| `MemoryRecalled` | `runId`, `namespace`, `query`, `resultCount`, `timestampMs` |
| `MemoryMessageSaved` | `runId`, `threadId`, `role`, `timestampMs` |
| `OpenApiToolCalled` | `runId`, `operationId`, `method`, `path`, `durationMs`, `status`, `timestampMs` |
| `TimerCreated` | `runId`, `timerId`, `firesAtMs`, `timerType`, `timestampMs` |
| `TimerFired` | `runId`, `timerId`, `firesAtMs`, `firedAtMs`, `delayMs`, `timestampMs` |
| `TimerCancelled` | `runId`, `timerId`, `timestampMs` |
| `AgentTraceEvent` | `runId`, `nodeId`, `iteration`, `attempt`, `trace`, `timestampMs` |
| `AgentTraceSummary` | `runId`, `nodeId`, `iteration`, `attempt`, `summary`, `timestampMs` |
| `AgentSessionEvent` | `runId`, `nodeId`, `iteration`, `attempt`, `transcript`, `timestampMs` |

`RunStateChanged` is typed and categorized for forward compatibility, but the current runtime does not emit it. Read derived run state with `getRun` or `computeRunState`, and watch `RunStatusChanged` for persisted status transitions.

`OpenApiToolCalled` is typed and categorized for forward compatibility, but the current OpenAPI tool factory records Effect metrics and log spans rather than emitting that event onto the run event bus.

## Agent Event Payload

This type describes the `.event` field of the `AgentEvent` variant above. For structured traces, prefer `AgentTraceEvent`, `AgentTraceSummary`, and `AgentSessionEvent`.

`AgentEvent.event` is the normalized CLI-agent event payload:

```ts
type AgentCliEvent =
  | { type: "started"; engine: string; title: string; resume?: string; detail?: Record<string, unknown> }
  | {
      type: "action";
      engine: string;
      phase: "started" | "updated" | "completed";
      entryType?: "thought" | "message";
      action: { id: string; kind: AgentCliActionKind; title: string; detail?: Record<string, unknown> };
      message?: string;
      ok?: boolean;
      level?: "debug" | "info" | "warning" | "error";
    }
  | { type: "completed"; engine: string; ok: boolean; answer?: string; error?: string; resume?: string; usage?: Record<string, unknown> };
```
