# 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):
- UI (per-component library reference, adapters, hooks, design guide)
- 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, losing work to a crash and forcing a manual restart. Smithers's render loop avoids that: each frame asks what's finished and what can start, validates task outputs against Zod schemas, and persists them to SQLite immediately. Crashes, restarts, and approvals become first-class, and the runtime resumes from the last persisted state without re-running completed work.
You are in the **Technical API**: reference material written for AI agents and
workflow authors, not for humans. You never need to read it; your agent does.
If you're a human, start at the **Product API**:
What Smithers Is explains the product in plain
language, and Platform Capabilities shows what to say to
your agent to get each capability.
**Default authoring path: MDX prompts.** Edit a plain Markdown file in `.smithers/prompts/`; the seeded TypeScript skeleton picks it up automatically, no build step or TypeScript needed. See MDX Workflow Authoring.
**Advanced authoring: TypeScript SDK.** For branching, schemas, parallel fan-out, or loops, write the workflow as a JSX tree that Smithers renders and drives execution from:
```tsx
{`Review ${ctx.input.repo}`}
{analysis ? (
{`Fix these issues:\n${analysis.issues.map(i => `- [${i.severity}] ${i.file}:${i.line} - ${i.description}`).join("\n")}`}
) : null}
```
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.
## Read next
- MDX Workflow Authoring to change agent behavior without writing TypeScript.
- Tour for a code-review example (TypeScript SDK).
- How It Works for the execution model.
- Why React? for the JSX runtime's rationale.
---
## Installation
> Install smithers-orchestrator with the workflow pack, or manually for standalone JSX workflow projects.
Most teams should start with the workflow pack: a working `.smithers/` directory with seeded workflows, prompts, and agent configuration, rather than hand-assembling the project structure.
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.
## Always Run with `bunx`
Agents, MCP configs, and docs should use `bunx smithers-orchestrator `.
Do **not** use `bunx smithers`: it's only the installed binary alias; on
npm `smithers` is an unrelated package, so `bunx smithers` can download and run
something else.
- The published npm package is [`smithers-orchestrator`](https://www.npmjs.com/package/smithers-orchestrator).
- `bunx smithers-orchestrator ...` works from any directory, using the project-pinned dependency when one exists.
- Use the `smithers` alias only when the environment intentionally provides that binary, e.g. a project script resolving `node_modules/.bin/smithers`.
- Avoid global installs: a global `smithers` on PATH can drift from and shadow the project-pinned version.
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 ` resolves the newest published version; inside a workflow project it uses the version pinned in `package.json`. To move a pinned project forward, bump the dependency, run `bunx smithers-orchestrator@latest init` to re-scaffold, or 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 update` do it: it detects the install method and runs the right command (or, for a `bunx`/project install, prints 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
```
Once the upgrade finishes, the update re-syncs every Smithers-owned skill (the
generated CLI command skills and the curated `smithers` skill) so an upgrade
never leaves an agent reading the previous release's `SKILL.md` and
`llms-full.txt`. This runs in agent and CI sessions too, not just interactive
terminals. Opt out with `SMITHERS_NO_SKILL_REFRESH=1`, and run
`bunx smithers-orchestrator skills add` yourself if the sync is skipped or
fails.
Verify the installed version:
```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 exists, or when its SOTA model registry falls behind the published one: run `smithers update` then `bunx smithers-orchestrator init` to move workflows onto the latest agents. Disable it with `SMITHERS_NO_UPDATE_CHECK=1` (already off in CI, JSON/agent output, and non-interactive shells).
### Clean reinstall
If you suspect a stale cache or leftover global symlink shadowing the project version, remove the global install and 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 a fresh fetch instead of 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/` | Public `create-workflow`, `create-skill`, and `docs-driven-development`, plus hidden system `init`, `post-failure`, and `upgrade` |
| `.smithers/prompts/` | Prompt templates for the curated authoring workflows |
| `.smithers/lib/ddd/` | Portable helpers for the docs-driven-development workflow |
| `.smithers/ui/` | Workflow-owned UIs and their complete local module closure |
| `.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`, `cursor.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 for ticket-oriented workflows |
| `.smithers/executions/` | Execution artifacts directory preserved across re-inits |
| `.smithers/.gitignore` | Ignore rules for generated workflow state |
The 29 former defaults are not installed; they remain complete, copyable graph
and UI examples under `examples/init-pack/` in the source repository.
To overwrite an existing scaffold:
```bash
bunx smithers-orchestrator init --force
```
## Install the Agent Skill
The `smithers` skill (for Claude Code, Codex, and other agents that drive
Smithers, not a GUI you click) makes it fluent without reading the whole docs
site first, so you reach the aha moment faster.
`init` auto-installs the curated Smithers skill into agents whose skill
directory Smithers can write today: Claude Code and Pi (no `mkdir`, no
`curl`). Other agents: use the MCP server plus standing instructions, or
point them at `docs-full` (below).
To sync the generated Smithers CLI command skill set:
```bash
bunx smithers-orchestrator skills add
```
That writes generated command-level skill files to supported skill locations,
including the canonical `~/.agents/skills` directory, and supports
`--no-global` for project-scoped installs, but unlike `mcp add`, has no
`--agent` target filter.
The curated onboarding skill ships the full docs bundle (`llms-full.txt`) next
to its `SKILL.md`, so agents read the exact API on demand. Once wired in, ask
for the outcome, e.g. *"orchestrate an agent to add rate limiting and keep
iterating until the tests pass,"* and it reaches for Smithers itself.
Agents without a skills directory: point them at
`bunx smithers-orchestrator docs-full` (prints the same bundle) or
`bunx smithers-orchestrator ask ""`.
Agent Support covers installing skills where supported and
registering the MCP server, across 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-` package vendors a jj binary per platform, so a fresh install needs no system jj. Resolution order:
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 is installed (unsupported platform, or `--no-optional`) and neither `jj` nor `git` is on `PATH`, worktree-needing runs fail with a message 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
Run a seeded workflow immediately.
Wire Smithers into your agent with a copy-paste setup prompt.
Make your coding agent fluent in Smithers.
The operational command cheatsheet.
Manual TSX authoring setup.
How a standalone workflow project fits together.
The built-in tool sandbox.
---
## Quickstart
> Scaffold and run a Smithers workflow in two commands.
The coding agent runs these commands. Agents: run them on the human's behalf, never
hand them off. Humans: ask your agent for the outcome; it runs these for you. Exception:
type the initial `init` yourself, before an agent exists.
`init` creates `.smithers/`: authoring workflows, DDD spec/UI closure, and agent
config. `--template ` requests a create-workflow-driven starter with follow-up
notes, not an archived workflow. Browse template IDs with
`bunx smithers-orchestrator starters`.
```bash
bunx smithers-orchestrator init
bunx smithers-orchestrator init --template idea-to-tickets
```
Point a seeded workflow at a prompt and Smithers starts a durable run.
```bash
bunx smithers-orchestrator workflow run create-workflow --prompt "Add a rate-limiting workflow"
```
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
```
Every completed step persists, so a crashed or stopped run resumes from its last
checkpoint.
```bash
bunx smithers-orchestrator workflow run create-workflow --run-id RUN_ID --resume true
```
`init` auto-installs the smithers skill for your coding agents. 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.
Full worked example: Tour. Every CLI command: CLI catalog.
---
## Starters
> Choose a plain-English outcome and the command your agent runs for it.
Starter requests describe an outcome before your agent writes workflow code. `init --template` installs the curated pack and returns a create-workflow request, never an archived starter workflow. Browse the gallery from any repo:
```bash
bunx smithers-orchestrator starters
```
First-time setup:
```bash
bunx smithers-orchestrator init --add-agents
```
Or initialize one template with guided next steps:
```bash
bunx smithers-orchestrator init --template idea-to-tickets
```
Template IDs: pass any to `init --template ` for a create-workflow request tuned to that outcome.
| Starter | Best for | Workflow | Command |
| --- | --- | --- | --- |
| `idea-to-tickets` | founders, product, operations, engineering | `create-workflow` | `bunx smithers-orchestrator init --template idea-to-tickets` |
| `launch-checklist` | launch owners and operators | `create-workflow` | `bunx smithers-orchestrator init --template launch-checklist` |
| `customer-incident` | support escalations | `create-workflow` | `bunx smithers-orchestrator init --template customer-incident` |
| `nontechnical-research` | before-build decisions | `create-workflow` | `bunx smithers-orchestrator init --template nontechnical-research` |
| `requirements-interview` | vague stakeholder requests | `create-workflow` | `bunx smithers-orchestrator init --template requirements-interview` |
| `quality-audit` | release readiness | `create-workflow` | `bunx smithers-orchestrator init --template quality-audit` |
| `test-coverage` | regression prevention | `create-workflow` | `bunx smithers-orchestrator init --template test-coverage` |
| `ship-a-change` | focused product improvements | `create-workflow` | `bunx smithers-orchestrator init --template ship-a-change` |
| `mission-mode` | larger approved milestones | `create-workflow` | `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 create-workflow
```
For another tool consuming the catalog, use JSON:
```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 last. 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` authorizes your Codex
subscription for workflow workers.
**Zod v4 is required.** Smithers introspects output schemas via Zod v4
internals; pin `zod@^4`, or a **v3** schema fails building an agent command
with the cryptic error `undefined is not an object (evaluating
'schema._zod.def')`.
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 Smithers-specific line; 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) => (
{{ message: `Hello, ${ctx.input.name}` }}
));
```
`createSmithers` registers Zod schemas as durable output relations the runtime manages. `outputs.greeting` is the typed reference for the `greeting` schema; using it as the `output` prop catches typos at compile time (`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, like `greeting` here, is an output table. Omitting `input` leaves `ctx.input` untyped, forcing a defensive guard on each field (`ctx.input?.name ?? "world"`). Fresh runs and graph previews parse input through this schema, so Zod defaults and transforms are available in `ctx.input`. Continue to coalesce fields declared optional or nullable.
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
```
A controller or custom monitor reads the same output through Gateway
`getNodeOutput({ runId, nodeId: "greet" })`, never the backing store directly;
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) => (
{`Review the code in ${ctx.input.repo} and return analysis as JSON.`}
));
```
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`, and once it appears on a later render frame, the downstream Task mounts.
When a Task consumes exactly one upstream output, `` with a `(deps) => ...` callback is more ergonomic; reach for `ctx.outputMaybe` to inspect content or gate multiple siblings.
```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 (
{`Review ${ctx.input.repo}`}
{analysis ? (
{`Fix these issues:\n${analysis.issues.map(i =>
`- [${i.severity}] ${i.file}:${i.line} - ${i.description}`
).join("\n")}`}
) : null}
);
});
```
Render 1 mounts only `analyze`. Render 2, once `analyze` finishes: `analysis` is populated, `fix` mounts and runs. That's the whole reactivity story: no hooks, no subscriptions, just JSX conditionals over persisted state.
The same shape covers branching, parallel groups, and loops: `?:` is the inline
conditional form, `` the declarative form for
explicit `then`/`else` (as props, not children):
```tsx
......... 0}
then={...}
else={...}
/>
......
```
## 5. An approval gate
Pause for a human: the runtime persists the decision and exits cleanly. The operating agent relays the question, then approves or denies through the CLI; resume continues from the gate.
```tsx
import { Approval } from "smithers-orchestrator";
{analysis ? (
{/* children rendered after approval */}
) : null}
{ctx.outputMaybe(outputs.confirmFix, { nodeId: "confirm-fix" })?.approved ? (
{`Apply patches`}
) : null}
```
Operator side (you, the agent, run these for the human; never hand them off):
```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 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)
```
In-flight attempts are marked stale and retried on resume. Same input + same code = same task IDs, so resume is deterministic.
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`, detailed in How It Works → Time travel.
- **Scorers** (attach evaluators to Tasks): Recipes → Scoring tasks.
- **Memory** (cross-run facts and message history): How It Works → Memory.
- **RAG**, **voice**, **OpenAPI tools**: opt-in fragments, indexed in llms.txt.
- **Tool sandbox** (read/grep/bash with path containment): 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.
API reference: Types lists every public type, its fields, and links to source and tests.
That loop is the entire model: everything below (branching, loops, approvals, resume, time travel) is either a JSX construct affecting rendering or a CLI surface over the persisted state.
## The render loop in detail
1. **Render**. The runtime calls your `smithers((ctx) => ...)` builder. React reconciles the returned JSX tree into 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 into 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 sequence has reached them, whose branch resolved them, and which fit `maxConcurrency`.
4. **Execute**. Each task runs in one of 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 depending on now-completed outputs mount on this frame and become eligible.
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` | Immutable input passed to `runWorkflow`. |
| `ctx.outputs(table)` / `ctx.outputs.` | `Row[]` | All rows for an output schema key or 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.outputRows(output, { nodeId?, scope? })` | `{ payload, nodeId, iteration, seq }[]` | Reads each durable row once in completion order. `seq` is stable across resume, rewind, and fork. |
| `ctx.latest(table, nodeId)` | `Row \| undefined` | A node's highest iteration; used inside `` for 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 \| 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 `` each pass writes a new row at the next iteration index.
The `table` argument is the schema key or output target from `createSmithers`
(`"review"` or `outputs.review`), not a raw SQL table name; the runtime resolves
it to the actual persisted table.
## Tasks: three modes
```tsx
// Agent: call an LLM. Children become the prompt; output validated against schema.
{`Review ${ctx.input.repo}`}
// Compute: children is a function. Runs at execution time.
{() => fs.readdirSync(ctx.input.dir).length}
// Static: children is a plain value. Persisted directly.
{{ region: "us-east-1", retries: 3 }}
```
If the agent declares `supportsNativeStructuredOutput = true` (`AnthropicAgent`
and `OpenAIAgent` by default), Smithers passes the Zod schema to
`agent.generate({ outputSchema })`, forwarded through the AI SDK's native
structured-output channel (`Output.object({ schema })`). Otherwise (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
// children execute top-to-bottom; default for // children execute concurrently
} else={}>
```
An explicit `` is only needed when nesting sequential groups inside `` or another control-flow primitive.
Use `.map()` and ternaries when the *number* or *presence* of tasks depends on state. Use `` and `` for fixed task sets whose execution shape depends on state.
`` is the one primitive that re-renders the same body repeatedly until `until` holds or `maxIterations` is reached (below). That cycle 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 it). Tasks emit outputs, the runtime persists them, and 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
Three consequences:
- The plan is a **derived value**, recomputed on every state change instead of mutated by hand.
- **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: the engine reuses one React root across frames, so hook state survives within a live process but is never persisted; a crash, resume, rewind, or fork starts a fresh process where every hook reinitializes. Use them only 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 isn't in the plan, with no "skipped" placeholder unless you use `` or `skipIf`. That's what lets `{analysis ? : null}` work as a clean dependency check. For one Task consuming one upstream output, `` 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
Deploy to production.
```
`` is a **decision node**: it produces a typed `ApprovalDecision` row that downstream rendering can branch on:
```tsx
{ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved
?
: }
```
Three denial policies (effects shown below): `"fail"`, `"continue"`, `"skip"`.
```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
```
`` is for richer interaction: a human submits arbitrary structured JSON. `` and `` 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
```
For resume to work, **task IDs must be stable across renders.** Derive them from data, not from indices or timestamps:
```tsx
{tickets.map((t) => )}
// NOT id={`work-${i}`} or id={`work-${Date.now()}`}
```
Same rule as React keys. A changed ID looks like a new task to the runtime; a disappeared one is dropped from the plan.
The supervisor auto-resumes runs whose owner process died:
```bash
bunx smithers-orchestrator supervise --all --interval 30s --stale-threshold 1m
```
Standalone supervision requires an explicit scope: pass one or more run IDs with
`--run`, or opt into the workspace-wide sweep with `--all`.
## 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 it with `fork`:
```tsx
Make a plan.Implement the plan.
```
`fork` is immutable: it copies the source conversation into a fresh, independent session, submits the new prompt, and leaves the source untouched, so many tasks can fork it in parallel and a forked task can itself be forked. Reading the snapshot from persisted state on each attempt makes fork resume-safe: the source is never re-executed. Inside a ``, `fork` resolves to the latest completed snapshot for that task id. See `` fork.
## Caching
Per-Task caching with explicit invalidation:
```tsx
({ repo: ctx.input.repo, version: "v3" }),
version: "v3",
}}
>
Analyze {ctx.input.repo}
```
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 rewind RUN_ID 5
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.
Before history is discarded or replayed, Smithers reads the effect journal.
Each marked tool call or Task moves through these states:
| Status | Meaning |
| --- | --- |
| `intended` | The journal row exists and execution started. |
| `succeeded` | Execution returned successfully. |
| `unknown` | Execution threw or stopped after it started. The external mutation may have happened. |
| `reverting` | A compensation handler is running. |
| `reverted` | Compensation finished. The row no longer blocks. |
| `revert-failed` | Compensation threw. The row still blocks. |
| `revert-stale` | The original call completed after compensation. The effect is active again and blocks or can be reverted again. |
`unknown` is deliberately conservative. A process can stop after an API
accepted a request but before Smithers records its response. The guard treats
`unknown` like `succeeded`.
```tsx
const announce = defineTool({
name: "announce",
schema: z.object({ channel: z.string(), text: z.string() }),
sideEffect: true,
idempotent: false,
execute: (args, ctx) =>
slack.chat.postMessage({ ...args, metadata: { key: ctx.idempotencyKey } }),
revert: async (args, ctx) => {
const message = await findMessageByKey(ctx.idempotencyKey);
if (message) await slack.chat.delete({ channel: args.channel, ts: message.ts });
},
});
```
A revert handler must be idempotent, verify before undoing, and tolerate
`effectStatus: "unknown"`. It should throw instead of guessing. Discard
commands run handlers in reverse effect order before changing VCS or database
history. `--no-revert` skips handlers; `--force` crosses what remains and
marks the run for attention.
The git exemption is exact: commits, ref changes, worktree writes, and
`git push` are not external effects. GitHub API state, including issues, PR
comments, and PR merge status, is external and must be marked.
## Scorers (evals)
Attach evaluators to a Task. They run **after** completion and never block.
```tsx
import { schemaAdherenceScorer, latencyScorer } from "smithers-orchestrator/scorers";
Analyze...
```
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.
Also exported: `runScorersAsync`, `runScorersBatch`, `aggregateScores`, the `smithersScorers` table, token-cost helpers (`modelTokenPrices`, `estimateCostUsd`), scorer metrics (`scorersStarted`, `scorersFinished`, `scorersFailed`, `scorerDuration`), and the side-effect analyzer APIs `sideEffectAnalysis` and `gradeSideEffectCompliance`.
Shared by the `eval-suite-run` seeded workflow and the `evals` gateway extension: `parseEvalDataset` (parses a JSON array or JSONL dataset), `evaluateEvalCase` (grades a case's status/output/error against `expected`: an assertion spec of `status`/`output`/`outputContains`/`errorContains`, or a literal value matched by subset/deep-equal), `evalAssertionScorer` (turns a graded case's assertions into a scored row), `evalCaseRunId` (a readable, collision-free child-run id), the `EVAL_CASE_STATUSES` and `EVAL_PASS_THRESHOLD` constants, and the low-level primitives `slugifyEvalToken`, `jsonEquals`, `jsonContains`, `normalizeExpected`, `formatEvalError`, and `isPlainObject`.
```bash
bunx smithers-orchestrator scores RUN_ID
```
## Memory (cross-run state)
Memory is **state that survives across runs**: namespaced facts and message history, not 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`; the opt-in `ElizaAgent` wraps an elizaOS `AgentRuntime` in-process too. All make plain HTTPS calls to a provider: **no subprocess, no container**, the agent's "environment" is your process. Only these agents 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.
**The default is: no container**, for either mode. A full OS environment is **opt-in** via ``. 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.
Use `AnthropicAgent`/`OpenAIAgent` for API-billed SDK agents with native
structured output. Use `ClaudeCodeAgent`, `CodexAgent`, and the other CLI
agents for the vendor CLI/subscription surface: they still produce typed task
outputs, via the engine's prompt-and-parse fallback unless the agent documents
a native opt-in.
## Common gotchas
- **Stable task IDs.** `id="implement-${i}"` or `id={Math.random()}` breaks resume. Derive from data.
- **`useState` is not durable.** It survives re-renders within one process but is lost on crash/resume/rewind/fork. 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).** On every boot the runtime runs `CREATE TABLE IF NOT EXISTS` and then `ALTER TABLE ... ADD COLUMN` for any column a typed input/output Zod schema introduced, so new fields are added in place without recreating `smithers.db`. If you still see `SQLiteError: table input has no column named X`, you're on a build before this boot-time migration landed (or a Postgres-backed DB, which doesn't auto-add columns yet); upgrade with `bunx smithers-orchestrator --version` or start a fresh run. Renaming or removing a field, or changing its type, isn't migrated and needs a fresh DB.
- **Code changes block resume.** A workflow source change is a different workflow: hot reload applies changes within a running frame, but resume validates the source hash of the original run, and a changed source blocks it. Start a new run instead of resuming 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 How it Works left you unsure *where an agent actually
runs*, what it costs, or which harnesses are "serverless", this page states the
execution model plainly.
## The default is: no container
The **agent class** decides execution mode, not a per-turn setting:
- **SDK agents run in-process.** `AnthropicAgent`, `OpenAIAgent`, and
`HermesAgent` extend the AI SDK's `ToolLoopAgent`; the opt-in `ElizaAgent`
wraps an elizaOS `AgentRuntime` in the same process. All make plain HTTPS
calls to a model provider: **no subprocess, 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`, spawning 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 driving the run. No automatic per-turn
container.
A full OS environment is always **opt-in**: wrap the work in
``.
### Three common guesses -- all reasonable, all wrong by default
Newcomers guess the same three things wrong:
| Guess | Reality |
|---|---|
| "Each agent turn spins up a fresh sandbox that pauses between turns." | No. By default nothing is containerized; containerization is an explicit `` choice, and pause/resume is a *provider* feature you opt into. |
| "OpenCode is serverless, Claude Code never is." | Half right: it's the auth mode that matters, not the vendor. Both go serverless via a cheap cold per-boundary container in API-key mode; only subscription mode (Claude Code's default) needs a warm container instead. See the compatibility table below. |
| "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": three independent axes, and a
given run may touch one, two, or all three:
| Axis | What it is |
|---|---|
| **Model tokens / subscription** | Billed per input/output token (SDK agents, or CLI agents in API-key mode), or drawn from a subscription quota (Claude Pro/Max, ChatGPT Plus/Pro) in subscription mode. |
| **Host compute** | A locally run CLI agent (the default) consumes the machine driving the run: your laptop, a CI runner, a long-lived server. |
| **Sandbox / provider compute** | Only when you opt into ``: 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: ``
`` 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, including the first-class Microsandbox provider.
The lifecycle at a `` 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 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`. The
Microsandbox provider exposes explicit create, start, stop, snapshot, and
cleanup semantics.
Running a CLI agent in a fresh per-turn container is a **composition you
author**, not a built-in switch: wrap the agent's `` in a child
workflow behind ``.
## 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, reading credentials from a one-time interactive
`/login` at `/.credentials.json`. `CodexAgent` mirrors
this with `CODEX_HOME` / `auth.json` against 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 (``) in API-key mode; 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 |
**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` (materializing git/jj worktrees on a real filesystem),
and the gateway is a `node:http` server: 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 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, worker-task dispatch falling back to in-memory message
storage instead of bun-sqlite); agent-task and gateway validation under
Node remain open. **Isolates** (Cloudflare Workers, Vercel Edge) are
further out, needing the same `node:fs`/`node:child_process` seam; a
Worker can host the **storage + in-process-agent** path today, not the run
driver.
## "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 `` 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.
---
## Provenance binding
> Bind a task to the exact upstream artifact that authorized it; stale authority parks instead of running.
A review approves a *specific* tree; a gate certifies a *specific* commit.
The moment the artifact changes, that authority is stale, but in a
re-rendering graph with loops and retries, nothing stops a task from reading
last iteration's approval and acting on it. Provenance binding makes "the
approval is only valid for the artifact it approved" an engine-enforced
property, not hand-rolled proof-id plumbing.
```ts
// In workflow code (render time):
const approval = ctx.prove("review", { nodeId: "lane:review" });
// → ProofBinding { table: "review", nodeId: "lane:review",
// iteration: 3, digest: "sha256:…" } | undefined
type ProofBinding = {
table: string;
nodeId: string;
iteration: number;
digest: string; // content hash of the bound output row
};
```
```tsx
Push the approved commit. …
```
## Semantics
- **`ctx.prove(table, { nodeId, iteration? })`** reads the latest (or the
named-iteration) output row of that node and returns a `ProofBinding`
carrying a content digest of the row, or `undefined` if the row doesn't
exist yet; like `ctx.outputMaybe`, it never throws at render time.
- **`bind`** on `` (a single binding or an array) is checked by the
engine **at schedule time**, not render time: the bound row's *current*
digest is recomputed and compared, and a mismatch parks the task as
`BOUND_STALE` instead of executing.
- A `bind` of `undefined` blocks scheduling like an unmet dependency: a task
cannot run on authority that was never produced.
## Staleness is a signal, not an error
`BOUND_STALE` does not fail the run: it's exposed to workflow code
(`ctx.boundStale("lane:push")`) and to `bunx smithers-orchestrator why`. On a
subsequent render, that signal can keep a correction loop open while the
upstream authority is reproduced against the current artifact:
```tsx
const pushStale = ctx.boundStale("lane:push");
const authorityIsCurrent = gate?.approved === true && !pushStale;
…
```
`until` is a boolean in the public API: a loop that has already completed
does not reopen merely because a downstream binding later becomes stale.
Keep the correction path active until the bound action is safe, or reproduce
the authority through an external/resume path before resuming the parked
task.
## Notes
- Bindings hash **content**, not iteration counters, so they survive resume,
retry, replay, and time travel: a restored run re-verifies against whatever
the rows say now.
- `ctx.boundStale(nodeId)` reports only the current task iteration: a newly
rendered iteration starts false even when an older one parked stale.
- Bind the *decision* row (the review verdict, the gate result), not raw agent
text; digesting is deterministic over the typed output row.
- Human approval and provenance binding answer different questions:
`` answers "may this happen at all";
a proof binding answers "is the thing that was approved still the thing
about to happen".
- `` and
`` are the canonical producers
and consumers: gate result rows and review verdicts get bound into the
landing step, so nothing stale can reach a push.
---
## 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: reliably finishing real work is a
**layered control system** living mostly *around* the model, not inside the prompt.
Smithers owns those 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 `` 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`, ``, ``, traces |
The first four shape what the agent *can* do; **backpressure** decides whether it's
allowed to move forward. A workflow that just tries its best and moves on has no
backpressure: that's 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 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 ``.
- **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: `` / ``.
- **Approval**: a human signs off before a risky action: ``.
- **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 (`` / ``) rather than running once
and hoping.
Command gates should classify failure evidence before marking work red: a
typecheck is red only on `tsc --noEmit` diagnostics like `error TS...`, and a test
run is red only on an actual failed-test report. A nonzero exit, signal, OOM, or
timeout with no such evidence is infrastructure failure, not proof the patch is
bad: retry with more headroom instead of surfacing it as red.
## Sequence for reversibility; isolate the irreversible
Order the work so reversible, low-stakes steps run first and the irreversible side
effect runs last, behind a gate: everything before it is safe to retry, replay, or
discard. The wire transfer, the production deploy, the email to every customer:
steps you can't take back, so push them to the end and guard them.
The sharper rule: split the decision from the act. An agent that both decides to
send money and sends it fuses a reversible step with an irreversible one: you
can't review or rerun the decision without risking the send. Don't 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.
Should we release the vendor payout? Return shouldPay, amount, and a reason.
;
// 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 && (
Wire ${payout.amount} to the vendor, keyed for idempotency.
);
```
This buys four things a fused step cannot: a typed decision you can read, score,
and replay; an act that's its own graph node, visible in traces and independently
approvable; a human confirming the exact amount before money moves; and a side
effect that stays idempotent on retry, since 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)). Same move
a database makes: do everything reversible, then commit once.
## 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 starts from zero, reading
only what's 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.** The worst kind to leave in: a wrong fact or dead
path becomes a false anchor the model tunnels toward. Distill a failed attempt
to one line ("tried X, failed because Y, do not repeat") and drop the rest.
2. **Add missing context.** What tools are for: a test run, a diff, a stack trace,
a file read, each turning an unknown into tokens to reason over. Without tools
an agent guesses; with them, it looks.
3. **Remove useless context.** Residue is a tax: a finished task's output is
adversarial noise for the next one. Rule of thumb: if you could `/clear`, you
should.
Compression scales all three at once: a good summary deletes the wrong, keeps the
missing, and discards the useless in one pass.
## Three levers, and they trade off
Three things you can optimize, and pushing one usually costs another; naming them
keeps you honest about which one you're 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're good enough: prove it,
then promote 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: `` and `` buy quality;
`` buys cost by running a cheap shadow model next to the primary task,
scoring both with the same scorer, and reporting the delta without touching the
result, so you see when the cheap model is ready to promote; `` 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 `` (three planners draft
independent plans, a moderator synthesizes one), then implements inside a
`` 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; the second hill pays more than the first.
The obvious hill is the **output**: generate, critique, regenerate, write the code,
run the reviewer, fix what it flagged. `` and `` 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, a synthesized plan instead of a cold
start. The swe-evo panel climbs both at once: it manufactures a better context (a
synthesized plan) before the first line of code, then the review loop climbs the
output after.
## The smart zone
Agents perform best under about 200k tokens of context, noticeably better under
100k; past that, attention thins and quality drops. Give an agent a goal it can
finish inside that budget, with research and planning already done, so its window
goes to the work, not discovery: that's why a research step and a plan step
precede implementation, keeping 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 ``,
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 scarcest resource is deciding how you will *know* it worked: spend it where
it's cheapest.
Pipeline stages cost wildly different amounts to review. Vibe-checking a finished
output is near free and lets debt pile up unseen; reading a 500-line diff is
miserable, you'll skim it; reading a *plan*, a page of intent before any code
exists, is cheap and high leverage. Put your eyeballs where they're cheapest:
review the plan, test the output, 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.
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, leaving 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"; a genuinely fuzzy goal can be "a reviewer
approves", preferring 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. Treat a missing or broken channel as fatal, stop and
fix it: don't keep optimizing against a phantom signal, or the agent will produce
confident work that satisfies a metric you can't trust. When you build a feature,
invest in the observability 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: an
agent that can't run the whole path can't tell whether it's done. Unit tests still
earn their place (TDD works well on small, self-contained snippets), but the e2e
test is what closes the loop.
A direct consequence: break the work into vertical slices, covered next.
## Break up tasks into vertical slices
When you decompose a system into tasks, cut it like this: boilerplate horizontally,
features vertically. Early tasks scaffold each level of the stack, the frontend
shell, the API skeleton, the database schema and migrations, because boilerplate is
uniform, low-risk, and parallelizes cleanly. Every task after that should be a
feature implemented end to end through all the levels, never a layer implemented
across all the features.
Two reasons the vertical cut wins for agents:
- **Backpressure is easier to implement.** A vertical slice terminates in behavior
a real e2e test can prove, so the gate writes itself: the checkout flow either
completes or it doesn't. A horizontal slice (a whole service layer for every
feature at once) has nothing real to validate until the final layer lands,
exactly when you want validation to have been happening all along.
- **Context stays colocated.** One feature's frontend, API handler, and schema
decisions live together in a single context window, so the agent holds the whole
path it is building. Split a feature across layer tasks and its context scatters
across windows: each task re-derives the others' contracts from scratch, and the
drift between them surfaces only at integration, the most expensive place to
find it.
## 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 Smithers's automatic
jj snapshotting instead of spending agent attention on git mechanics.
This generalizes into **sandwich delegation**: smart, expensive agents plan and
review at the two ends, cheaper agents implement in the middle, recursively as the
work grows (a capable model can write a Smithers script whose `` plans with
two strong models and whose `` validates, while cheaper models
implement in between). The more cost-insensitive you are, the more of the middle
you can hand to a strong implementer, but never spend your most expensive model on
work a cheaper one can do without reason.
## The lifeline rule: protect the orchestrator's own context
The orchestrator driving a long run is the one context alive for the whole job:
every sub-agent is disposable, it 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 comes 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 have it 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 stays clean shouldn't also hold every artifact.
``, ``, and `` exist so verification lives apart
from the thing being verified. Keep the orchestrator lean and it runs 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 doesn't announce itself.
The cheap fix: re-read. Every few steps of a long job, and always after a
`` handoff, re-read the spec or goal and this doctrine, then check
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, which is why durable `/clear` re-injects the
distilled goal on every fresh window: a clean context that's 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. It
optimizes for speed and cost, never quality, since you're going to discard it:
only the lessons survive, and those feed the plan. Treating a 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: 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's 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 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 human fix 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:
- `` sets a hard ceiling on the loop's context; a breach
throws `ASPECT_BUDGET_EXCEEDED`.
- `` catches exactly that
code instead of failing the run.
- The catch branch renders ``, 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:
*
* a hard token ceiling for the subtree. The
* engine enforces it at task dispatch; a breach
* throws ASPECT_BUDGET_EXCEEDED.
* an error boundary that catches exactly that
* code and renders the catch branch instead of
* failing the run.
* 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.
* 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 /; 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
// ). Graph rendering does not run the agent; these are the real
// flags a live run needs.
const worker = new ClaudeCodeAgent({
model: "claude-opus-5",
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 | 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 (
}
try={
{prompt}
}
/>
);
});
```
## 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 **Technical API** docs set, which is written for agents, not
humans. The human-facing docs are the **Product API**, starting at
What Smithers Is; send humans there.
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 ` (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. |
Route a most-trivial edit that takes only a few turns directly. Route any clear
single-goal task through `smithers oneshot`, which launches quickly in the
background with a live chat and diff UI; one strong agent finishes repo-wide
goals of up to roughly 300k tokens in a single oneshot run, so a large goal
alone never justifies a workflow. Use a full workflow when the task is
genuinely multi-goal in shape: staged phases, approval gates, durable loops,
parallel work, or a need for reuse. Clarify ambiguous goals before launching
either form.
## 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:
- `` for parallel command or agent checks with one pass/fail verdict.
- `` for scan -> fix -> verify -> report loops.
- `` or `` 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 ` 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 `, 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.
---
## Oneshot
> Run one well-scoped goal with a single strong agent in the background, with optional review and a live dashboard. No workflow file to author.
`smithers oneshot` is the built-in minimal workflow: one agent, one goal, no
authoring. It launches in the background by default and serves a live
dashboard with chat, diff, hijack, pause, and cancel controls.
```bash
bunx smithers-orchestrator oneshot "add a loading spinner to the login button"
bunx smithers-orchestrator oneshot --goal-file TICKET.md
```
## When to use it
Route work in three tiers:
1. **Trivial asks** (a typo, a rename, an edit under about 10 agent turns): do
them directly. If the stored trivial preference is `oneshot`, launch oneshot
with `--model opus` or `--model terra`: those are the only two slots allowed
for trivial oneshot (never sol, luna, kimi, or any other tier), and no other
model belongs in a trivial routing explanation. Trivial means one tiny
change in one place: an ask that bundles multiple coordinated edits (a flag
plus its help text plus a test, code plus docs) is tier 2 oneshot work, not
trivial, no matter how fast it would be.
2. **Clear single-goal asks**, small or repo-wide: `smithers oneshot`. One
strong agent routinely finishes hours-long goals in a single oneshot run of
up to roughly 300k tokens, so a large goal is not a reason to leave this
tier. The worker manages its own context across the run, so "it will not
fit in one context window" is not a reason either. When explaining a
routing decision, name the task shape and the seat it routes to (UI goals
lead with kimi: opencode, then pi, then the kimi CLI, backed by claude
opus or fable; every other goal leads with claude opus) and which seat
you picked.
3. **Genuinely multi-goal work** (human approval gates, staged phases that
need different agents or models, parallel fan-out, durable loops, or a
reusable procedure): build and run a real workflow.
Size does not pick the route; shape does. A task with one finish line belongs
in oneshot no matter how much work it implies. Real asks that fit a single
oneshot run, each historically completed by one strong agent in under 300k
tokens:
- "Go through the entire codebase and make sure every feature is documented."
- "Run `pnpm up --latest` on every package and make sure all builds still pass."
- "Make CI green on this branch: rebase on main, fix failures, push until green."
- "Replace every use of library X with library Y and get all tests passing."
- "Read review.md, address every review comment, delete the artifacts when done."
Authoring a workflow for asks like these is overengineering: it pays authoring
latency and review overhead for durability the task does not need. Escalate to
tier 3 only when the task requires a workflow-only feature (an approval gate,
phases needing different models, parallel lanes, reuse). Neither "it feels
big" nor the existence of a seeded workflow with a matching name (`audit`,
`review`, `upgrade`) qualifies: shape decides, not the catalog. Pick exactly
one route and commit to it; never answer with a menu of alternative routes or
a hybrid of oneshot plus a workflow.
Explicit overrides win over inference: "oneshot" forces oneshot, "oneshot with
review" adds `--review on`, "oneshot without review" adds `--review off`.
Ambiguous goals deserve clarifying questions to the user before anything
launches; wait for the answers rather than substituting assumptions or an
exploratory plan for them. "Make the settings page better" gets a reply that
is ONLY clarifying questions, covering both the target (which settings page?)
and the goal (what is wrong today? what does better mean? what counts as
done?). The entire reply is the questions: never a plan whose first step is to
find out, and never a provisional plan under an assumed answer.
## Agents and models
With no `--model` or `--agent`, oneshot classifies the goal and routes by task
shape (registry v8). A UI-flavored goal (interface, page, component, styling,
layout, responsive, animation, theme, dashboard, and similar keywords) leads
with Kimi K3: OpenCode's `kimi-for-coding/k3` seat first, then kimi through
the Pi CLI (`pi --provider kimi-coding --model k3`), then the Kimi CLI's
`kimi-code/k3`. When no kimi seat is usable, the UI chain falls back to Claude
Opus 5, then Fable 5; Codex Sol is only ever the last-resort UI rung. Every
other goal, including tedious backend work (migrations, backfills, renames,
test burndowns), runs the default chain: Claude Opus 5 first (the default
implementer), then Codex Sol, then Kimi K3, then Fable, with pi's kimi-coding
seat closing the chain.
Every rung is availability-gated: oneshot selects only from the agents
detected usable on the machine, the claude seats fall back to opencode when
the Claude CLI is unavailable, and the kimi seats all run Kimi K3 (1M-token
context). An explicit `--model` (a slot: `sol`, `terra`, `luna`, `kimi`,
`fable`, `opus`, `sonnet`; or a canonical model id) or `--agent` (`codex`,
`kimi`, `claude-code`, `opencode`, `pi`) always overrides classification; the
default `auto` follows the task shape, with review on Sol at high reasoning
effort.
Run `bunx smithers-orchestrator oneshot --status ""` before first use.
It prints the usable agents, the goal's classified `taskType`, the resolved
model chain, and the stored preferences as JSON; when no usable agent is
detected, oneshot is unavailable and work should go through the direct or
workflow route instead. Oneshot being unavailable never means the
orchestrating agent is unavailable: a simple task still gets done directly, a
multi-goal one still gets a workflow, and "no routing path exists" is never
the answer.
## Review and trivial preferences
Two stored preferences shape routing, kept in the global Smithers config:
- **Review** (`--set-review on|off`): add one review-and-polish round after the
implement pass. Higher quality, slower. Recommended on.
- **Trivial** (`--set-trivial direct|oneshot`): whether the most-trivial asks
run directly or still launch oneshot. Recommended direct.
`--review on|off` overrides the stored review preference for one run.
## Dirty working copies
Oneshot runs directly in `--cwd`, so a stale detached lineage, foreign WIP, or
`.jjconflict*` tree can be swept into a goal commit and make landing unsafe.
The default `--preflight auto` warns and prepends agent-judged triage
instructions; use `warn` to warn without injection or `off` to skip assessment.
## Watching and steering a oneshot
Every launch CTA tells the operator to offer the monitor to the user. Open it
directly with:
```bash
bunx smithers-orchestrator monitor ONESHOT_RUN_ID
```
For a built-in oneshot, this opens the dedicated oneshot monitor. It includes
the live implement and review transcripts, diff, durable event log, pause and
cancel controls, plus:
- **Steer**: send a message from the chat composer. Delivery is shown as
`queued`, `delivered`, `agent-acked`, or an explicit failure. Claude Code is
currently supported by interrupting at the next recorded agent-event
boundary, resuming the same Claude session with the appended message, then
resuming the Smithers run. If delivery fails after handoff, the monitor clears
the hijack request and returns control to Smithers so the run is not stranded.
Codex, Kimi, and OpenCode are shown as unsupported
because their current headless sessions do not provide an equally reliable
live message boundary. Use `bunx smithers-orchestrator hijack RUN_ID` for
interactive takeover.
- **Restart**: after confirmation, cancel the active attempt if needed and
launch a fresh run from the durable `builtinResume` argv recorded at the
original launch. The UI follows the new run id.
- **Cheap narrator**: while the monitor remains attached, the Luna/trivial
model tier tails recorded agent activity and emits short status lines. It
stops after the attachment lease expires, so an unattended oneshot does not
pay narration cost. Lines are persisted as run events and reappear after a
monitor reattaches.
The oneshot UI, the `chat-create` run UI, and the Monitor's node-row hijack
view are all the same shared `OneshotSurface` component from
`smithers-orchestrator/gateway-ui`: goal and status cards, chat, diff, events,
and an embedded PTY terminal for hijack and reopen. The Monitor hosts it in a
dialog that can maximize to the viewport and restore.
Steering and restart requests and outcomes are durable run events shown by
`bunx smithers-orchestrator status RUN_ID`, `events RUN_ID`, and
`timeline RUN_ID`. Narrator lines are durable `NodeOutput` events shown by
`events RUN_ID --raw` and by a later monitor attachment.
On an interactive TTY, `--interactive` opens the full-screen TUI monitor
instead of the detached default. `--detach false` runs in the foreground.
## Overriding the built-in workflow
A workspace can replace the built-in pipeline with
`.smithers/workflows/oneshot.tsx`; the CLI passes `goal`, `review`, and
`model` input fields to it, and uses `.smithers/ui/oneshot.tsx` as the
dashboard when present. See Custom workflow UIs.
---
## 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 outputs, and re-renders. Branching, looping, and parallelism are normal JSX.
API reference: Authoring and Components list every authoring helper and JSX element, with options and links to source and tests.
## 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 at `smithers-orchestrator/jsx-runtime`: skip `react` and `react-dom`. `jsxImportSource` (below) routes JSX through the workflow reconciler, never React DOM. The one required dev dep is `@types/react` (React ships no bundled types; the JSX transform resolves its namespace from it). Only the browser UI surface, `smithers-orchestrator/gateway-react`, 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) => (
{{ summary: `Analyze ${ctx.input.repo}` }}
));
```
`createSmithers` is a named export; the lowercase `smithers` it returns is not a separate top-level import. `smithers((ctx) => ...)` returns the `SmithersWorkflow` value the workflow file exports. The `input` schema above types `ctx.input.repo`; omit it and `ctx.input` is `unknown`, so guard or parse it yourself.
`outputs.analysis` is the typed reference for the Zod schema, so typos are compile errors. The task body here is a JSX expression (`{...}`) returning a plain object: a static return, no LLM call. Real tasks pass a `run` prop or an AI model; see the Task component reference.
## Reactivity
The tree re-renders every frame. `ctx` is just an optional function argument: drop it and write `smithers(() => ( ... ))` if a workflow never reads `ctx.input` or `ctx.outputMaybe`. When used, `ctx` exposes `ctx.input` and `ctx.outputMaybe(ref, { nodeId })`, which returns a completed task's output, or `undefined` if it hasn't run yet:
```tsx
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
{analysis ? ... : null}
```
The `report` Task doesn't exist in the plan until `analysis` completes: no placeholder, no skipped node, the conditional IS the dependency. Static DAG tools require declaring optional nodes upfront; the JSX conditional instead evaluates fresh each frame, so `report` simply isn't there while `analysis` is undefined.
For a task that just consumes one upstream output, `` 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.
## 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 ` (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 `, `--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 ` resolves IDs from the nearest local `.smithers/workflows/.tsx`, local `.smithers/packs/*/workflows/`, global `~/.smithers/workflows/`, and global `~/.smithers/packs/*/workflows/`. Local workflows take precedence: on an id collision the local file wins. Run `workflow run :` to select a pack explicitly. 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 `` 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.
## Run concurrency
`up` and `workflow run` begin with a run-wide cap of 4 concurrent tasks when
you omit `--max-concurrency`. Smithers raises that default automatically when
queued work shows that the workflow needs more slots. Demand-driven raises stop
at `SMITHERS_AUTO_MAX_CONCURRENCY_CEILING`, which defaults to 16. Set that
environment variable to a positive integer to choose a different automatic
ceiling; an absent or invalid value uses 16.
Passing `--max-concurrency N` (or `-c N`) is an explicit pin, not a starting
value. It disables both demand-driven raises and automatic raises derived from
the workflow's declared `` width, so the run never exceeds `N`.
The explicit value is not limited by `SMITHERS_AUTO_MAX_CONCURRENCY_CEILING`:
use `--max-concurrency 64` when you intentionally want a 64-task cap.
Without an explicit pin, a workflow that declares a wider `` or `subtreeConcurrency={N}` starts at that declared width,
even when `N` exceeds the automatic ceiling. The ceiling applies only to
additional demand-driven raises. See Parallel for how
the run-wide and group caps compose.
## Launch attribution
`up`, `workflow run`, `oneshot`, and `chat-create` accept
`--started-by-harness`, `--started-by-session`, and `--started-by-prompt`.
They persist optional self-reported launch provenance; the prompt is never
copied from workflow input, `--prompt`, or a oneshot goal. Short-lived CLI
launches best-effort infer Codex/Claude harness/session markers when those
identity flags are omitted. Use explicit fields for Kimi, OpenCode, and other
harnesses with a known session.
## 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
```
A detached launch (`up --detach`, `workflow run --detach`, detached `--resume`) can fail before any run exists: a workflow-file parse/import/graph error exits non-zero with a `file:line:col` error, and an engine that dies before the run is admitted exits non-zero with the child's log tail. Tooling that parses `--detach` output should treat a missing run ID plus non-zero exit as "no run was created".
## 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 \"\""`); 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 --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`. A built-in oneshot opens its dedicated transcript, steer, restart, and attachment-scoped cheap narrator surface. Other runs open the all-runs workspace view focused on that run (status, execution tree, events, approvals). 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/.tsx` UI with ``, 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 ` 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 `` 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 ` for a remote Gateway. When the Gateway requires a bearer token, `ui`/`monitor`/`gui` route the browser through its session handoff so the printed URL opens cleanly; pass `--host 0.0.0.0` (or `SMITHERS_GATEWAY_HOST`) for Tailscale/LAN access, which auto-mints a token and prints dialable interface URLs.
## 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-` 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`.
## Time-travel commands compared
Five commands cross historical boundaries. Pick by what each acts on:
| Command | Acts on | Behavior |
| --- | --- | --- |
| `rewind` | a run | Rewinds a run to a previous frame. Args: `RUN_ID FRAME_NO`. |
| `replay` | a snapshot checkpoint | Forks from a checkpoint and resumes execution (time-travel fork). Takes a workflow. |
| `fork` | a snapshot checkpoint | Creates a child run. `--run` also resumes it. |
| `timetravel` | a past task state | Reverts filesystem plus DB state to a previous task state, then optionally resumes. Takes a workflow. |
| `revert` | the workspace | Reverts the workspace to a previous task attempt's filesystem state only, no DB reset. Takes a workflow. |
Every command prints an effect-boundary report, including a clean report when
no effects are crossed. `revert`, `timetravel`, and `rewind` run registered
compensation handlers before discarding history. Pass `--no-revert` to skip
them; skipped effects become blockers. `replay` and `fork --run` never
compensate the parent run's effects.
```bash
bunx smithers-orchestrator revert workflow.tsx \
--run-id RUN_ID --node-id NODE_ID --attempt 1 [--force] [--no-revert]
bunx smithers-orchestrator timetravel workflow.tsx \
--run-id RUN_ID --node-id NODE_ID [--force] [--no-revert]
bunx smithers-orchestrator rewind RUN_ID FRAME_NO \
[--force] [--no-revert]
bunx smithers-orchestrator replay workflow.tsx \
--run-id RUN_ID --frame FRAME_NO [--force]
bunx smithers-orchestrator fork workflow.tsx \
--run-id RUN_ID --frame FRAME_NO --run [--force]
```
Without `--force`, crossed `succeeded`, `unknown`, `revert-failed`, or
`revert-stale` effects stop the operation with
`TIME_TRAVEL_SIDE_EFFECT_BLOCKED`. With
`--force`, Smithers records `SideEffectBoundaryCrossed` and marks the run for
attention. A plain `fork` without `--run` does not re-execute anything, so it
reports a warning instead of blocking.
Git commits, branch and ref changes, worktree writes, and `git push` are
exempt. GitHub API mutations such as issues, comments, and PR merges are not.
See Time-travel API and Revert for the programmatic surface.
## Command catalog (TOON)
Commands listed by dotted name. `human` and `alerts` use an action positional instead of nested subcommands.
```toon
commands[102]:
- name: add
purpose: Install a workflow pack from GitHub, npm, or a local file
args[1]{name,type,required,desc}:
spec,string,true,GitHub npm or file pack spec
flags[2]{name,short,type,default,desc}:
global,,boolean,false,Install in ~/.smithers/packs instead of the local project
yes,,boolean,false,Skip trust confirmation
- 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[38]{name,short,type,default,desc}:
detach,d,boolean,false,Background mode; preflight the graph, then print runId/pid/logFile once the run is admitted
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
parent-run-id,,string,,Existing run ID to record as this run's parent (persisted lineage, surfaced by inspect/ps and the MCP run tools)
accept-workflow-change,,boolean,false,Resume this run after its workflow source changed, re-blessing durability metadata in place; you own replay determinism
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)
monitor,,boolean|string,true,Monitor workflow that watches this run as a sibling; auto-discovers .smithers/monitor/.tsx (no-op when absent). Pass a path to pick one; --no-monitor opts out
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[40]{name,short,type,default,desc}:
detach,d,boolean,false,Background mode; preflight the graph, then print runId/pid/logFile once the run is admitted
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
parent-run-id,,string,,Existing run ID to record as this run's parent (persisted lineage, surfaced by inspect/ps and the MCP run tools)
accept-workflow-change,,boolean,false,Resume this run after its workflow source changed, re-blessing durability metadata in place; you own replay determinism
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)
monitor,,boolean|string,true,Monitor workflow that watches this run as a sibling; auto-discovers .smithers/monitor/.tsx (no-op when absent). Pass a path to pick one; --no-monitor opts out
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)
started-by-harness,,string,,Durable self-reported launch harness; environment may fill when omitted
started-by-session,,string,,Durable self-reported harness session; environment may fill when omitted
started-by-prompt,,string,,Explicit durable launch context; never inferred from workflow input
- 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/.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 explicitly named stale runs and auto-resume them; --all opts into a workspace-wide sweep
flags[6]{name,short,type,default,desc}:
run,r,string,,Only supervise these run IDs (comma-separated)
all,a,boolean,false,Explicitly supervise every eligible run in the workspace
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. A built-in oneshot opens its dedicated live transcript, honest steering delivery, restart, and attachment-scoped cheap narrator controls. Other runs use the all-runs workspace view. 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; built-in oneshots open their dedicated monitor
flags[6]{name,short,type,default,desc}:
gateway,g,string,,Gateway base URL (default http://127.0.0.1:)
port,,number,7331,Gateway port when --gateway is not set
host,,string,,Gateway bind host when autostarting (or SMITHERS_GATEWAY_HOST); use 0.0.0.0 for Tailscale/LAN access; a bearer token is minted automatically for non-loopback binds
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[9]{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 from the live cursor
interval,i,number,2,Watch poll seconds
history,,boolean,false,Replay existing history before tailing in watch mode
- 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[5]{name,short,type,default,desc}:
agent,,enum,,claude-code|codex|antigravity|pi|kimi|amp
cwd,,string,.,Working directory for the chat session
started-by-harness,,string,,Durable self-reported launch harness
started-by-session,,string,,Durable self-reported harness session
started-by-prompt,,string,,Explicit durable launch context
- name: oneshot
purpose: Run one well-scoped goal with a strong agent in the background, with optional review and a live UI
args[1]{name,type,required,desc}:
goal,string,false,Goal to complete; required unless using --status or a preference setter
flags[15]{name,short,type,default,desc}:
goal-file,,string,,Read a long goal from a file
model,,string,auto,Model slot or canonical model id
agent,,enum,,codex|kimi|claude-code|opencode
review,,enum,stored,Review preference for this run (on|off)
set-review,,enum,,Persist the review preference (on|off)
set-trivial,,enum,,Persist trivial-task routing (direct|oneshot)
status,,boolean,false,Print usable agents model chain and stored preferences as JSON
cwd,,string,.,Working directory for the task
preflight,,enum,auto,Dirty-working-copy preflight (auto|warn|off)
detach,d,boolean,true,Run in the background; --detach false runs in the foreground
interactive,,boolean,false,Open the full-screen TUI monitor
open,,boolean,true,Open the run UI after launch
started-by-harness,,string,,Durable self-reported launch harness
started-by-session,,string,,Durable self-reported harness session
started-by-prompt,,string,,Explicit durable launch context; never inferred from the goal
- 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: status
purpose: "Concise run health at a glance: verdict, node counts, agent/model mix, throughput, and the nodes gating progress."
args[1]{name,type,required,desc}:
runId,string,true,Run ID to summarize
flags[2]{name,short,type,default,desc}:
json,,boolean,false,Output the structured summary as JSON
window,,number,,Recent-activity window in minutes for the throughput/verdict checks (default 10)
- 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[6]{name,short,type,default,desc}:
gateway,g,string,,Gateway base URL (default http://127.0.0.1:)
port,,number,7331,Gateway port when --gateway is not set
host,,string,,Gateway bind host when autostarting (or SMITHERS_GATEWAY_HOST); use 0.0.0.0 for Tailscale/LAN access; a bearer token is minted automatically for non-loopback binds
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[5]{name,short,type,default,desc}:
gateway,g,string,,Gateway base URL (default http://127.0.0.1:)
port,,number,7331,Gateway port when --gateway is not set
host,,string,,Gateway bind host when autostarting (or SMITHERS_GATEWAY_HOST); use 0.0.0.0 for Tailscale/LAN access; a bearer token is minted automatically for non-loopback binds
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[6]{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
force,,boolean,false,Cross unresolved effects and mark the run needs-attention
revert,,boolean,true,Run compensation handlers; use --no-revert to skip
- 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[6]{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
accept-workflow-change,,boolean,false,Resume this run after its workflow source changed, re-blessing durability metadata in place; you own replay determinism
- 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[9]{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
revert,,boolean,true,Run compensation handlers; use --no-revert to skip
- name: replay
purpose: Fork from a checkpoint and resume execution
args[1]{name,type,required,desc}:
workflow,string,true,Workflow file path
flags[7]{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
force,,boolean,false,Cross unresolved effects and mark the parent needs-attention
- name: fork
purpose: Create a branched run from a snapshot checkpoint
args[1]{name,type,required,desc}:
workflow,string,true,Workflow file path
flags[7]{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
force,,boolean,false,Allow --run to cross unresolved external effects
- 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: packs
purpose: List and update installed workflow packs.
- name: packs.list
purpose: List local and global workflow packs.
- name: packs.update
purpose: Re-resolve installed packs from their locked specs (all packs when no name is given).
args[1]{name,type,required,desc}:
name,string,false,Pack name to update (default: every locked pack)
- name: remove
purpose: Remove an installed workflow pack.
args[1]{name,type,required,desc}:
name,string,true,Installed pack name
flags[1]{name,short,type,default,desc}:
global,,boolean,false,Remove from ~/.smithers/packs
- name: eject
purpose: Copy a pack workflow and its UI, prompts, and libraries into the local .smithers pack.
args[1]{name,type,required,desc}:
spec,string,true,Pack workflow in the form :
- name: share
purpose: Add this project's workflow pack to awesome-smithers and open a pull request.
flags[2]{name,short,type,default,desc}:
repo,,string,,Override the awesome-smithers repository (owner/name)
dry-run,n,boolean,false,Print the registry entry and diff without pushing
- 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[4]{name,short,type,default,desc}:
yes,,boolean,false,Skip confirmation prompt
json,j,boolean,false,Emit JumpResult JSON
force,,boolean,false,Cross unresolved effects and mark the run needs-attention
revert,,boolean,true,Run compensation handlers; use --no-revert to skip
- name: snapshots
purpose: List durability snapshots (workspace checkpoints) for a run and its descendant child runs
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 and invalidate child work newer than it
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: worktree.list
purpose: List every worktree Smithers created in this repository and the run that owns it
- name: worktree.prune
purpose: Remove the worktrees of runs that are over (finished, failed, or cancelled)
flags[4]{name,short,type,default,desc}:
run,,string,,Only prune worktrees owned by this run id
older-than,,string,,Only prune worktrees untouched for at least this long e.g. 24h
dry-run,,boolean,false,Report what would be removed without removing anything
force,,boolean,false,Also remove worktrees holding uncommitted or unpushed work
- 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). Workflow packs update via `packs update`.
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[41]{name,short,type,default,desc}:
detach,d,boolean,false,Background mode; preflight the graph, then print runId/pid/logFile once the run is admitted
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
parent-run-id,,string,,Existing run ID to record as this run's parent (persisted lineage, surfaced by inspect/ps and the MCP run tools)
accept-workflow-change,,boolean,false,Resume this run after its workflow source changed, re-blessing durability metadata in place; you own replay determinism
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)
monitor,,boolean|string,true,Monitor workflow that watches this run as a sibling; auto-discovers .smithers/monitor/.tsx (no-op when absent). Pass a path to pick one; --no-monitor opts out
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)
started-by-harness,,string,,Durable self-reported launch harness; environment may fill when omitted
started-by-session,,string,,Durable self-reported harness session; environment may fill when omitted
started-by-prompt,,string,,Explicit durable launch context; never inferred from workflow input
- 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; also subscribes the session's claude monitor to the run
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 the runs this session subscribed to (claude tick / claude subscribe) and print one NDJSON line per actionable transition (approval pending, human request, failed, stalled, node retry churn), plus a periodic run-progress digest whenever a followed run has been silent for a full window; --transitions all adds finished/cancelled/continued, --all-runs follows every run in the workspace; backs the plugin's background monitor
flags[7]{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
retry-alert-attempt,,number,3,Active-attempt number that flags a node as retry-churning (0 disables)
progress-every-ms,,number,1800000,Emit a run-progress digest after this long without any notification for a followed run (0 disables)
ticks,,number,,Stop after N polls (default run until killed)
transitions,,string,actionable,Which transitions stream: actionable or all
all-runs,,boolean,false,Follow every run in the workspace instead of only this session's subscriptions
- name: claude.subscribe
purpose: Subscribe this session's background monitor to a run (done automatically by claude tick and Claude-launched runs); the monitor only notifies about subscribed runs
args[1]{name,type,required,desc}:
runId,string,true,Run ID the session's monitor should follow
- name: claude.unsubscribe
purpose: Stop this session's background monitor from following a run (outside a Claude Code session it drops the run for every session)
args[1]{name,type,required,desc}:
runId,string,true,Run ID the session's monitor should stop following
- 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`): preflight-renders the workflow graph before spawning anything (a parse, import, or graph error prints `file:line:col` on stderr and exits non-zero with no run created), then redirects stdout/stderr to a log file, waits up to 10s for proof the run was admitted, prints `runId`/`pid`/`logFile`, and exits. A child that dies before admission prints its log tail and exits non-zero instead of a success run ID. The same preflight and admission gate covers `workflow run --detach` and detached `--resume`.
- **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. `events --watch` on a live run starts at the current event cursor and only prints events appended after startup; pass `--history` (or a `--since` window) to replay matching history first.
- **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
```
## Notes
- Without `poll` the component runs once; with `poll` it wraps in a Loop.
- Given a comparison output, `alertIf` decides whether to render `alert`; without `alertIf`, the trigger is `comparison.drifted === true`.
- Without `alert`, the component compares but takes no action.
---
##
> 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
};
```
Complete runnable example (renders with `bunx smithers-orchestrator graph`):
```tsx
import { createSmithers, ScanFixVerify, ClaudeCodeAgent } from "smithers-orchestrator";
import { z } from "zod";
const { Workflow, smithers, outputs } = createSmithers({
scan: z.object({ issues: z.array(z.string()) }),
fix: z.object({ patch: z.string() }),
verify: z.object({ resolved: z.boolean() }),
report: z.object({ summary: z.string() }),
});
const agent = new ClaudeCodeAgent({ model: "claude-sonnet-5" });
export default smithers(() => (
Scan the codebase for linting errors and type issues.
));
```
## Notes
- The loop always runs all `maxRetries` cycles; early exit on verifier output isn't wired yet, so it ends via `onMaxReached: return-last`.
- The report task always runs, even when retries are exhausted.
---
##
> 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);
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
Check whether the deployment to production has completed successfully.
```
## 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 ``: the run parks as
`waiting-timer` between attempts, surviving a crash or resume instead of
sleeping in-process. A detached run resumes via the gateway's timer sweep.
- `intervalMs` doesn't bound how long a check may run; use `checkTimeoutMs` to
cap a single attempt.
---
##
> 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;
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(() => (
backup.sql", risk: "risky" },
{ id: "run-migration", command: "npx prisma migrate deploy", risk: "critical" },
{ id: "smoke-test", command: "npm run test:smoke", risk: "safe" },
]}
/>
));
```
## Notes
- Each step depends on the previous via `needs`, guaranteeing execution order.
- Critical steps set `elevated: true` in approval metadata for stronger auth UIs.
- Approval output lives at `{prefix}-{step.id}-approval-decision`.
---
##
> 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; // { 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(() => (
Build the user authentication module with tests.
));
```
## Notes
- Generated node ids: `{id}-plan`, `{id}-loop`, `{id}-worker-{type}`, `{id}-review`, `{id}-final`.
- Workers run with `continueOnFail`; one failure doesn't abort the cycle.
- With `useWorktrees`, each worker runs in `.worktrees/{prefix}-worker-{type}` on branch `worker/{prefix}-worker-{type}`.
---
##
> 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
priority?: number; // inherited task priority; default 1000
failurePolicy?: "halt" | "quarantine";
skipIf?: boolean;
children?: ReactNode;
};
```
```tsx
{items.map((it, i) => (
{{ value: i }}
))}
```
## Notes
- Innermost group determines the effective cap for its descendants; tasks
outside the queue are unaffected.
- Default priority `1000` lets ready landing work claim a free run slot before
ordinary tasks at priority `0`; an explicit child priority wins.
- `failurePolicy="quarantine"` lets unrelated queue branches continue after a
child fails; default `halt` fails the run.
---
##
> 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>;
verdictOutput: OutputTarget;
strategy?: "all-pass" | "majority" | "any-pass"; // default: "all-pass"
maxConcurrency?: number; // default: Infinity
continueOnFail?: boolean; // default: true
skipIf?: boolean;
};
```
```tsx
```
## Notes
- Check task ids are `{prefix}-{checkId}`; verdict is `{prefix}-verdict`.
- `strategy` is pure code: `all-pass` needs every check to pass, `majority` needs more than half (`passCount*2 > total`), `any-pass` needs at least one.
- Use `command` instead of `agent` for shell-based checks.
---
##
> 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;
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",
});
;
```
## Notes
- Two-phase: the first render classifies; the next uses `classificationResult` to mount route handlers.
- Each entry's `category` must match a `categories` key; unmatched entries are skipped silently.
- Route tasks default to `continueOnFail`.
---
##
> 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;
synthesizer: AgentLike;
gatherOutput: OutputTarget;
synthesisOutput: OutputTarget;
gatheredResults?: Record | null; // typically from ctx.outputMaybe()
maxConcurrency?: number; // default: Infinity
synthesisPrompt?: string;
skipIf?: boolean;
children?: ReactNode; // overrides synthesisPrompt
};
```
```tsx
```
## Notes
- Synthesis task auto-receives `needs` for every source, gating it on all gathers.
- Source `children` takes priority over `prompt`.
- `gatheredResults`, when provided, folds into the default synthesis prompt.
---
##
> 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;
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
Review the changes in src/auth/ for security, quality, and architecture concerns.
```
## Notes
- Panelist task ids: `{prefix}-{label|role|panelist-N}`; moderator is `{prefix}-moderator`.
- Panelists may share a `label`/`role` (two `security` reviewers): colliding ids get an index suffix (`{prefix}-security-1`), so every panelist keeps its own task.
- `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.
---
##
> 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
```
## Notes
- Task ids: `{prefix}-proposer`, `{prefix}-opponent`, `{prefix}-judge`, loop `{prefix}-loop`.
- Loop runs exactly `rounds` iterations, `onMaxReached="return-last"`.
- Proposer and opponent share `argumentOutput`, differentiated by task id.
---
##
> 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, "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; // 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; // content for onComplete task
};
```
Complete runnable example (renders with `bunx smithers-orchestrator graph`):
```tsx
import { createSmithers, Kanban, ClaudeCodeAgent } from "smithers-orchestrator";
import { z } from "zod";
const { Workflow, smithers, outputs } = createSmithers({
triage: z.object({ note: z.string() }),
build: z.object({ note: z.string(), done: z.boolean() }),
});
const agent = new ClaudeCodeAgent({ model: "claude-sonnet-5" });
export default smithers(() => (
[{ id: "T-1" }, { id: "T-2" }]}
maxIterations={2}
/>
));
```
## 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.
---
##
> Recursive tiered delegation with risk probes, per-node backpressure, live edits, budgets, and scoring.
`` is the composite behind the
`delegation-chain` workflow: a `` of
seven phases in `` with a live-edit signal listener. Each phase
re-derives its slice of the delegation tree from the `dc*` output rows on
every render, so fan-out materializes level by level, and edits or probe
findings replan the affected subtree without a restart.
```ts
// Props
import { DelegationChain } from "smithers-orchestrator";
type Tier = "fable" | "opus" | "sonnet" | "haiku";
type DelegationAgents = Partial>;
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) => (
));
```
Register `delegationSchemas` in `createSmithers` and pass the matching
`outputs` subset. Tiers are labels only: missing ones fall back to the
nearest configured tier in `tierOrder`. Setting `budget.maxMinutes` wraps the
chain in `` 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 |
| --- | --- | --- |
| `` | Question forecast, prefetched forms, one durable question at a time, refined-prompt approval | `prompt`, `maxQuestions`, `prefetchDepth` |
| `` | Recursive decomposition fan-out until the frontier is all leaves | `prompt?` (standalone root brief; otherwise waits for the approved goal) |
| `` | Zero-backpressure expected-output previews, skippable via `dc-skip-preview` | none |
| `` | Every node declares gates and dependencies before execution | none |
| `` | Risk probes, findings to the nearest parent, replan/reaffirm cascades | none |
| `` | Dependency-ordered leaf pipelines: exec, gates, approvals, developer previews, budget guards | `maxAttempts` |
| `` | Run-level score digest plus satisfaction poll | none |
| `` | 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` needs an
`approvalPolicy`, `dcBudget` needs a `budget`, and `dcScore` needs run-level
`scorers`.
| Table | One row per |
| --- | --- |
| `dcGoal` | Refined goal (prompt, assumptions, questions asked) |
| `dcQuestion` | Rendered question form / resolved answer |
| `dcForecast` | Goal agent's upfront question batch (internal) |
| `dcGoalApproval` | Human's `{ approved, refinedPrompt }` decision |
| `dcPlan` | Delegating node's plan: children, risks, estimates; replans append superseding rows |
| `dcPreview` | Leaf's never-executed expected output |
| `dcGates` | Node's declared gates and logical dependencies |
| `dcProbe` | Probe's finding (`planImpact`: changes / confirms / none) |
| `dcReplan` | Replan decision (`invalidated` or `reaffirmed`) with its trigger |
| `dcExec` | One execution attempt, with best-effort `actual` usage and measured `commitRange` |
| `dcReview` | Review/check gate verdict for one attempt |
| `dcDevPreview` | Developer-preview build (`builtOk` required to pass) |
| `dcApproval` | Approval-gate decision |
| `dcEdit` | Live user edit (signal payload) |
| `dcSkip` | Skip-previews signal payload |
| `dcPoll` | End-of-run poll answers |
| `dcBudget` | Budget-guard checkpoint (`ok` or `warn`) |
| `dcScore` | Run-level scoring digest |
## Physical node ids
Every task id follows `dc::` (`idPrefix` defaults to `dc`).
Logical ids are `/`-separated paths (`root/core/reducer`); physical ids
encode the `/` 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-`,
`dc:goal:question-` (the durable human answer),
`dc:goal:goal`, `dc:goal:approve`
- Planning: `dc::plan`, replan versions `dc::plan-`
- Previews: `dc::preview`
- Gates: `dc::gates`
- Derisk: `dc::probe-`, `dc::replan-`
- Execution: `dc::exec`, `dc::review-` (reviews then
checks, in declared order), `dc::approval-`,
`dc::dev-preview` (then `dev-preview-2`, ...),
`dc::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 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 `dcExec.commitRange`, with
instructions to inspect the commits themselves; chunk-level reviews get the
union of their subtree's ranges (exec agents wrap with the exported
`withCommitRange`, measuring the working-copy commit before and after the
attempt: jj first, 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 `submitEdit`,
`skipPreviews`, `answerHuman`, and `submitPoll`. The pure reducer
`foldDelegation` is exported for tests and non-React use.
## Notes
- Approval gates require `outputs.dcApproval`; `` throws
`INVALID_INPUT` if the policy produced approval gates without it.
- `dcPlan.orchestration` (`"tasks" | "workflow"`) is reserved for
higher-order orchestration (a node authoring its own workflow as its
execution strategy); accepted but 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 scorer reference).
---
##
> Recursive, model-authored delegation over a strict and fuel-bounded workflow IR.
`` lets Sol or Fable decide the workflow shape from evidence found
during the run: authors return declarative `agent | sequence | parallel`
data, and Smithers validates and compiles it into ordinary tasks in the same
run. Terra and Luna only execute bounded goals; their schemas can't delegate.
Trellis is experimental and coexists with the fixed
``: use it for open-ended
work with runtime-adaptive topology, and keep fixed JSX for known
deterministic graphs.
```tsx
import {
ClaudeCodeAgent,
CodexAgent,
Trellis,
createSmithers,
delegationV2Schemas,
} from "smithers-orchestrator";
import { z } from "zod/v4";
const input = z.object({
prompt: z.string().trim().min(1).default("Build the requested result and prove it works."),
});
const { Workflow, smithers, outputs } = createSmithers({
input,
...delegationV2Schemas,
});
const sol = new CodexAgent({ model: "gpt-5.6-sol" });
const fable = new ClaudeCodeAgent({ model: "claude-fable-5" });
const terra = new CodexAgent({ model: "gpt-5.6-terra" });
const luna = new CodexAgent({ model: "gpt-5.6-luna", model_reasoning_effort: "high" });
export default smithers((ctx) => {
// The schema default is applied before runtime and static graph rendering.
const prompt = ctx.input.prompt?.trim() || "Build the requested result and prove it works.";
return (
);
});
```
The run must pin the same concurrency value:
```bash
bunx smithers-orchestrator up workflow.tsx \
--input '{"prompt":"Implement the feature and prove it works"}' \
--max-concurrency 4
```
Trellis rejects an omitted or mismatched run cap, and rejects
`requireRerenderOnOutputChange: false`, to keep recursive fan-out in one
authoritative scheduler pool and ensure outputs trigger the continuation
render consuming them.
## Props
```ts
type TrellisProps = {
prompt: string;
goal?: GoalContract;
acceptance?: AcceptanceCriterion[];
instructions?: string;
role?: "sol" | "fable"; // default "sol"
work?: WorkKind; // default "synthesize"
outputContract?: OutputContractId; // default "work_product"
agents: Record<"sol" | "fable" | "terra" | "luna", AgentLike | AgentLike[]>;
outputs: TrellisOutputs; // from delegationV2Schemas
idPrefix?: string; // default "trellis"
semanticRevision?: string;
criticalExecutionPolicy?: {
allowedCategories: Array<
| "security_boundary"
| "data_integrity"
| "concurrency_invariant"
| "protocol_core"
| "irreversible_migration"
>;
allowedPathPrefixes: string[]; // normalized workspace-relative paths
maxChangedLines: number; // 1..500, per admitted execution
};
maxConcurrency?: number; // default 4; must equal the pinned run cap
limits?: {
maxTotalAuthorTurns?: number; // default 32; global recursive author fuel
maxAuthorGenerations?: number; // default 4 per logical author
maxAuthorDepth?: number; // default 4
maxNodesPerProgram?: number; // default 64
maxProgramDepth?: number; // default 8
maxFanout?: number; // default 16
maxPromptBytes?: number; // default 16,384
maxTotalPromptBytes?: number; // default 131,072
};
skipIf?: boolean;
};
```
Spread `delegationV2Schemas` into `createSmithers` and pass its `outputs`: it
registers raw author/worker envelopes, validation rows, canonical outcomes,
the final row, and reserved question/answer rows.
`semanticRevision` is a caller-owned cache/identity revision: bump it when an
agent's provider, sandbox, ambient commands, or tool policy changes invisibly
to the agent fingerprint, so Trellis derives new root/final node IDs instead
of reusing stale semantic results.
### Exceptional Sol/Fable execution
Direct Sol/Fable implementation is disabled unless `criticalExecutionPolicy`
is set. With a policy, an authored `execute` node must:
- request a registered criticality category
- state the invariant and line sensitivity
- name workspace-relative paths and expected changed lines
- identify surrounding delegated work
- feed its output to an independent review node that is (or feeds) a
declared program output, so the next author continuation can't race it
Independent means a different role backed by a non-overlapping agent or
failover chain, beyond merely a different graph node. Trellis derives the
allowed reviewer-role matrix from `agents`: executor and reviewer can't share
an `AgentLike` object or a non-empty `AgentLike.id`, and any failover-chain
overlap disqualifies that pairing.
The validator admits requests only inside the caller's category, path-prefix,
and line ceiling. The compiler binds the canonical grant to the exact
invocation, program ID/digest, logical node, role, `execute` work kind, and
the reviewer's role/outcome node: an ungranted or mismatched Sol/Fable
`complete` becomes `runtime_failed/invalid_return`. Policy and grant hashes
persist in task metadata; changing policy changes the root semantic identity.
Phase A ships this as an admission contract only: the runtime doesn't compare
the reported estimate to a real diff or prevent symlink/ambient-shell escapes.
Configure an adapter-owned sandbox/tool boundary for those guarantees.
## Authority and return types
Every model return is a strict top-level object containing a tagged union:
| Renderer | Allowed final tags | May author descendants |
| --- | --- | --- |
| Sol/Fable author | `subworkflow | complete | blocked` | Yes |
| Sol/Fable semantic repair | `subworkflow` only after settlement rules | Correct one rejected fragment |
| Terra worker | `complete | blocked` | No |
| Luna worker | `complete | blocked` | No |
| Trusted settlement | `complete | blocked | runtime_failed` | No |
| Root final row | `complete | blocked | runtime_failed | fuel_exhausted` | No |
An authored subworkflow can contain Sol, Fable, Terra, or Luna `agent` nodes
inside `sequence` and `parallel` containers: Sol/Fable nodes recursively
render another author invocation, Terra/Luna nodes render one worker plus one
deterministic settlement, and workers can't smuggle graph structure since
`subworkflow` is absent from their output schema.
The runtime never trusts a model's terminal claim directly: settlement binds
it to the trusted assignment's role, work kind, output contract, and exact
acceptance IDs, and every passed or failed criterion must cite a real
evidence or artifact ID. Bad coverage, a dangling proof, the wrong work kind,
or a contract-specific shape violation becomes parent-visible
`runtime_failed/invalid_return`.
## Recursive execution
One logical author invocation proceeds:
1. Sol/Fable returns `complete`, `blocked`, or a proposed subworkflow.
2. A deterministic validation node receives a bounded raw JSON proposal and
rejects unknown fields/tags, bad roles,
unresolved/forward references, unsafe parallel writes, invalid critical
execution, contract mismatches, and resource-limit violations.
3. One semantic-repair author turn may correct rejected IR: a structural diff
permits changes only at diagnosed fragments and necessarily affected
references, so valid nodes, intent, and continuation state can't be
replaced. Rejection never mounts descendants.
4. Accepted IR compiles inline into the current run and preserves explicit
declared-output fan-in.
5. Every child settles to a canonical outcome; the author receives a bounded,
provenance-bearing evidence packet and either finishes or authors the
smallest corrective fragment.
This expresses direct delegation, pipelines, fan-out/fan-in, research/POC
waves, debate, and review/optimization loops; the loop is the author
continuation over settled evidence, rather than a model-authored JavaScript
condition.
Accepted history is append-only. Physical IDs include runtime/prompt/registry
versions, the root assignment, author lineage, generation, semantic program
digest, logical ID, and phase: reordering parallel siblings doesn't change the
digest, but changing sequence order or prompt semantics does. Completed rows
are replay-safe without treating a mutable logical ID as identity.
## Fuel and concurrency
`maxTotalAuthorTurns` hard-caps all Sol/Fable calls, including semantic
repair. After an accepted fragment, remaining turns split deterministically
between the parent continuation and immediate nested authors by sorted
logical ID; shares are immutable and non-refundable, so unused child fuel
burns and siblings can't race or reset a shared counter. Author tasks get no
execution retries (`maxSchemaRetries={0}`). At the last permitted depth,
Sol/Fable may still author Terra/Luna work, but validation blocks another
Sol/Fable child from mounting.
Trusted task metadata persists the pinned root concurrency, global author-turn
cap, generation/depth limits, and each invocation's immutable allocation and
local remainder, so workflow UIs can distinguish selected-run truth from
next-launch controls. A root invocation's remainder isn't the global
remainder once fuel splits into child allocations.
An authored `parallel.maxConcurrency` may add a smaller local cap but can't
exceed the pinned root value. This release rejects nested local caps that
Smithers can't compose faithfully.
## Output contracts
Work intent and output shape are separate. The closed registry supports
`work_product`, `goal_contract`, `plan`, `evaluation`, `classification`,
`issue_scan`, `condition`, `artifact_collection`, and `evidence_collection`;
the validator restricts which work kinds may promise each contract, and
settlement checks contract-specific cardinality or structured detail.
Unfavorable evidence is still a completed result: a failed review, disproven
POC, or failed preview is not automatically `blocked`. Runtime crash, timeout,
cancel, invalid return, fuel exhaustion, and invalid subworkflow remain typed
separately.
## Phase A limitations
- The nonblocking `ask_question` broker isn't wired: prompts say questions
are unavailable, and the UI offers no answer control.
- `Task.allowTools` isn't an authority boundary for every adapter: prompt
policy can't stop a shell-capable agent from invoking ambient commands, so
configure role agents with a real sandbox/tool policy. The example UI
labels this boundary explicitly rather than presenting prompt policy as
enforced tool or filesystem isolation.
- Hierarchical token, USD, and time reservations are not enforced.
- Only `agent | sequence | parallel` is authorable; branches, loops, macros,
approvals, arbitrary callbacks, child runs, and model-authored schemas are
intentionally unavailable.
The repository includes a real custom UI and example workflow documented at
`trellis`.
---
## Recipes
> Tight, reusable patterns. Copy, paste, adapt.
Each recipe is a working snippet plus one line of context. They compose freely.
API reference: Reference overview maps every package to its exhaustive API page, each with links to source and tests.
If you want a ready-to-run outcome before writing workflow code, start with Starters or run `bunx smithers-orchestrator starters`.
## Default model routing
Claude builds and gates; Codex reviews and validates. Implementation runs on
Claude Opus 5 (registry v7: it outbenchmarks GPT-5.6 Sol on agentic coding at
lower output cost), and when usable Codex authentication is available GPT-5.6
checks the work: Sol for final review and second opinions, Terra for
validation and tool-heavy checking, and Luna only for trivial, minimal-risk
passes (tiny scoped edits, mechanical transforms, quick lookups,
research-style gathering). Pin the tier to the role:
| Work | Model |
|---|---|
| Trivial, minimal-risk edits, cheap passes, and research gathering | `gpt-5.6-luna` |
| Substantial implementation: the default build seat | `claude-opus-5` |
| Validation, structured checking, and tool-heavy verification | `gpt-5.6-terra` |
| Final code review, second opinions, and the deepest Codex reasoning | `gpt-5.6-sol` |
| Orchestration and gating: scope, direction, progress, done-or-not | `claude-opus-5` |
| Planning, design freezes, and premium judgment calls | `claude-fable-5` |
Never give GPT-5.6 Sol or Terra the orchestration or gating seat: deciding
scope, choosing direction, judging whether work progresses, and calling
something done are Claude jobs (Opus 5 at medium reasoning effort by
default, Fable 5 for planning and the most consequential calls). Sol and Terra
stay excellent inside reviews and validation, where they report findings for
the Claude orchestrator to weigh. Implementation starts on Claude Opus 5
(registry v7: it outbenchmarks Sol on agentic coding at lower output cost),
escalating to Fable 5 for the most ambitious builds; do not let Luna
carry work with real blast radius. 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. 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
{`${ctx.input.task}\nPrior review: ${ctx.latest(outputs.review, "review")?.feedback ?? "none"}`}
{`Review the latest implementation. Return { approved, feedback }.`}
```
Stop conditions must be measurable (boolean, count, array length). Avoid "looks good" prompts; agents are literal.
In a `` `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
```
`continueOnFail` keeps one tier's timeout from blocking the other.
## Approval gate with branching
Decision data drives the next branch.
```tsx
{ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved
?
: }
```
`onDeny`: `"fail"` aborts, `"continue"` proceeds without the gated branch, `"skip"` skips the gated tasks.
## Retry policy & timeouts
```tsx
Call external API.
```
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
Run lint checks. Pipeline continues if this fails.
```
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" ? (
{`Critical: ${analysis.summary}`}
) : 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 (
} />
{unfinished.map((t) => (
))}
);
});
```
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
{`Analyze config files in ${ctx.input.dir}, find bugs, fix them, write results.
Use read, edit, bash. Return { summary, filesChanged }.`}
```
## 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-terra", instructions: "...", tools: { read, write, edit, bash } });
```
Match the tool surface to the role. Luna carries only trivial, black-and-white
work (quick research, mechanical transforms); substantial implementation starts
on Terra, and the hardest or most ambiguous changes escalate to Sol.
## Side-effect tools with idempotency
External mutations must mark themselves and use the runtime idempotency key.
Add `revert` when time travel can compensate the operation.
```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 });
},
async revert(args, ctx) {
const ticket = await jira.findIssueByKey(ctx.idempotencyKey);
if (ticket) await jira.deleteIssue(ticket.id);
},
});
```
Retries reuse the same idempotency key, so a successful side effect from attempt 1 isn't doubled by attempt 2.
`revert` is legal only with `sideEffect: true`. Its context includes
`output`, `effectStatus`, `idempotencyKey`, `runId`, `nodeId`, `iteration`,
`attempt`, and `toolCallSeq`. `effectStatus` is either `succeeded` or
`unknown`. Treat both as "possibly happened": find the external object, then
undo it. A handler must be idempotent and must throw when it cannot verify the
object safely.
## Caching for iterative authoring
```tsx
({ repo: ctx.input.repo }), version: "v2" }}
>
{`Analyze ${ctx.input.repo}`}
{(deps) => `Report on ${deps.analyze.summary}`}
```
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 `` 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 commit ID in `_smithers_attempts.jj_pointer` per attempt. Revert any attempt with a recorded JJ pointer to its exact workspace state:
With `SMITHERS_DURABILITY_SNAPSHOTS=1`, attempts in one run that share a
`rootDir` can overlap while their brief JJ captures are serialized. Different
run scopes sharing that worktree, including a parent and child run, serialize
the whole attempt so checkpoints retain the correct owner. A waiter occupies a
scheduler slot for up to five minutes. A timeout disables snapshots for that
waiter and records a durability gap that needs attention. Lock waits are logged
and abort-aware.
```bash
bunx smithers-orchestrator revert workflow.tsx --run-id RUN_ID --node-id implement --attempt 1
```
Every discard command checks the external-effect journal before changing the
worktree or run state. Registered handlers run first. An unresolved effect
stops the command with `TIME_TRAVEL_SIDE_EFFECT_BLOCKED`.
```bash
# Print the report and cross unresolved effects. The run is marked for attention.
bunx smithers-orchestrator revert workflow.tsx \
--run-id RUN_ID --node-id implement --attempt 1 --force
# Skip registered handlers. Any crossed effects now require --force.
bunx smithers-orchestrator revert workflow.tsx \
--run-id RUN_ID --node-id implement --attempt 1 --no-revert --force
```
Git commits, branch changes, worktree writes, and `git push` are exempt.
GitHub API mutations such as `gh pr merge` are not.
## 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. Branch operations do
not run compensation handlers because the parent still owns its effects.
`replay` and `fork --run` stop before an effect-bearing boundary unless
`--force` is present. A plain `fork` succeeds and reports a warning.
`--restore-vcs` checks out the original revision so re-execution sees the same
source. See Time-travel commands compared.
## Scoring tasks
```tsx
import { schemaAdherenceScorer, latencyScorer, llmJudge } from "smithers-orchestrator/scorers";
`Rate the analysis quality.\nInput: ${JSON.stringify(input)}\nOutput: ${JSON.stringify(output)}`,
}),
sampling: { type: "ratio", rate: 0.1 },
},
}}
>
Analyze...
```
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/.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
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
Understand the bug and identify possible fixes.
Try the minimal fix.
Try the refactor fix.
```
`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 `` it forks the latest completed snapshot for that id. See `` fork.
## Read next
- How It Works: the model these recipes plug into.
- Components: full prop surface.
- CLI: every command.
---
## Workflow Authoring Rules
> The rules that repeatedly cost authoring runs when learned at runtime instead of before render. Read this before hand-authoring a workflow.
These five rules keep biting workflow-authoring agents at runtime, hours after
writing a workflow, instead of surfacing at the first `bunx smithers-orchestrator graph`.
Each is documented elsewhere in full; this page collects all five in one read
before you write JSX, instead of hitting them one at a time across failed
runs. See
[the postmortem](https://github.com/smithersai/smithers/blob/main/research/workflow-authoring-friction-postmortem.md)
this page was written to close.
## 1. Output schemas cannot reuse the reserved key columns
Output tables get a fixed `run_id` / `node_id` / `iteration` key prefix; input
tables get `run_id` only. **Why:** these columns correlate a row back to the
run, node, and loop iteration that produced it, so a schema field named
`runId`, `nodeId`, or `iteration` collides with the reserved one.
```ts
const outputs = z.object({
// runId / nodeId / iteration: DON'T, reserved, collides at construction
summary: z.string(),
});
```
A collision throws `INVALID_INPUT` at construction (schema build time), not
at run time; see `zodToTable`.
## 2. No nested loops, use the queue-based backfill pattern instead
A ``/`` as the **literal immediate JSX child** of another throws
`NESTED_LOOP` at graph-extraction time (before any agent runs). **Why:** with
nothing between them, there's no clear "whose iteration is this" for the
inner loop, the same gap
Effect combinator builder's `G.loop` rejects
unconditionally.
```tsx
{/* throws NESTED_LOOP */}
```
Fix, per the error message: route inner work through a queue like
`` and re-enter via the outer loop's next iteration.
Narrower than it sounds: a `` reached through a
``/``/`` wrapper (not the literal immediate
child) is a **different, genuinely-supported shape**. Each ``-forked
lane (one per array item or isolated ``) may run its own bounded
correction loop, scoped to the outer loop's iteration: the sanctioned
"per-item lanes, each with a correction loop" pattern, exactly what
`close-issues.tsx` and `studio-parity-swarm.tsx` do (outer discover step, then
a `` of per-item `` lanes, each with its own correction
``), regression-guarded by
[`nested-loop-runtime.test.jsx`](https://github.com/smithersai/smithers/blob/main/packages/engine/tests/nested-loop-runtime.test.jsx)
(issue #117): the inner loop's state and cache reset correctly per outer
iteration. Avoid only a *second* loop governing the *same* lane with nothing
forking between them.
## 3. `ctx.latest` vs `outputMaybe({ nodeId, iteration })` for loop bindings
Inside a ``'s `until`, read the most recent iteration with
`ctx.latest`:
```tsx
```
**Why:** `ctx.outputMaybe(schema, { nodeId })` with no explicit `iteration`
resolves the *current render iteration*, which equals the loop's own
iteration only for a single, non-nested loop, and is `0` when several
coexist (siblings, or one nested under a ``/`` fork per
rule 2). An `outputMaybe`-based `until` built on that ambient iteration can
silently never advance: it spins to `maxIterations` and returns the last
result, with no error explaining why. To use `outputMaybe` directly, pass the
loop's own scoped node id and iteration:
`ctx.outputMaybe(schema, { nodeId: "review", iteration: N })`.
## 4. Workflow tests must render the real graph via `renderWorkflow`, not a hand-built one
Authoring a workflow and writing its testing-library test are one indivisible
change. A workflow delivered without that test is unfinished work, not a
follow-up opportunity.
Import the actual workflow module and drive it through
`renderWorkflow`
(`smithers-orchestrator/testing`):
```ts
import { renderWorkflow } from "smithers-orchestrator/testing";
const render = async (name, input = {}, outputs = {}) =>
renderWorkflow(await load(name), { workflowPath: join(workflowsDir, name), input, outputs });
```
**Why:** a test that hand-builds its own plan/graph object (bypassing
`extractGraph`/`buildPlanTree`) can pass while the real workflow file has a
typo, a `NESTED_LOOP`, or a reserved-column collision: it validates a stand-in
that merely resembles the workflow. `renderWorkflow` exercises the same
extraction path `bunx smithers-orchestrator graph`/`bunx smithers-orchestrator up` do, so a
graph-level defect fails the test the same way it fails a real run.
Assert real graph behavior: exact node ids and dependency order, representative
valid and malformed values through each task's `outputSchema`, and branch or
loop behavior under the outputs that drive it. A truthiness smoke test such as
`expect(graph).toBeTruthy()` does not meet this rule.
## 5. New `.smithers` test files must be registered in `.smithers/package.json`
`.smithers/package.json`'s `test` script is an explicit, space-separated list
of test file paths, not a glob. **Why:** pack tests run outside the normal
per-package `bun test tests` convention (they share fixtures/agents across
many workflow files), so there's no directory-wide default to fall back on. An
unregistered test file is silently never run by `pnpm test` or CI: it can sit
green-looking in the repo indefinitely while contributing zero coverage.
`node scripts/check-smithers-test-script.mjs` (part of the root `pnpm test`
gate) catches this; run it after adding a test file, or just add the path
yourself.
---
## Common Footguns
> The handful of mistakes that bite people first, and the pattern that avoids each.
Smithers is durable by design: a run replays from persisted state, not memory, and that's the source of most of the sharp edges below.
## Resume and state
### Unstable task IDs break resume
The runtime keys completed work by task `id`: a changed id looks like a new task, a disappeared one drops from the plan. Derive ids from data, never a loop index or timestamp.
```tsx
{tickets.map((t) => )}
// NOT id={`work-${i}`} or id={`work-${Date.now()}`}
```
Same rule as React keys; see How It Works.
### Input is immutable after the first run
A run's `--input` is persisted at start; resuming with different input errors rather than overriding it silently. Start a new run instead.
### Code changes block resume, they do not merge
A workflow source change is a different workflow: resume checks the source hash, so editing the file blocks it. Start a new run, or hot-reload (`up --hot`) a still-running one: edits apply to newly scheduled tasks while in-flight tasks finish on their original code. See Recipes.
### `useState` is not durable
React state resets every render, i.e. every frame here. Anything that must survive a crash belongs in a Task output read back through `ctx`, not component state.
### Evidence files must be run-scoped or cleaned
If a workflow writes verdict files like `artifacts/.../verify.json` and reads them back for a done-check, put `ctx.runId` in the path, or delete the evidence directory at run start: a prior run's `verify.json` must never satisfy a fresh run's done-check.
## Caching
### Do not cache side-effecting tasks
`cache` is for pure, expensive-to-recompute work: caching a deploy, email, or mutation means it silently skips on a hit. The key is `cache.by(ctx)` plus `cache.version` plus the output schema signature, so a schema change auto-invalidates it, and a stale cached row fails validation, missing safely. See How It Works.
## Side effects and retries
### Mark side-effecting tools and key them
Tasks retry, and a retried tool call can fire twice: declare `sideEffect: true` on a custom tool and pass `ctx.idempotencyKey` through to the downstream system so a retry is a no-op, not a second charge. The key stays 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 stops double-charging on retry but doesn't make the
charge reviewable: an agent that decides to send money and sends it can't
have that decision inspected or rerun without risking a second charge. Have the
deciding task return a typed decision (`{ shouldPay, amount, reason }`) and put
the payout in its own downstream task behind an
``: the decision stays reversible and
replayable, the act 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`/`grep`, an implementer gets `write`/`edit`/`bash`, and an agent with no `tools` can't 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` built with just a `model` prompts for
permission before editing files: fine interactively, but in a detached run
(`up -d`) nothing clicks to approve it, so the task stalls until its heartbeat
timeout. Construct autonomous-run agents with the bypass flags:
```tsx
const opus = new ClaudeCodeAgent({
model: "claude-opus-5",
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,
});
```
`.smithers/agents.ts`'s named pools are intentionally bypass-free for
interactive use; don't 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 live process; it can't retroactively unblock a suspended run, like
one paused at an approval gate. Resume still rejects an
edited file, now with `RESUME_METADATA_MISMATCH`; changing task IDs or the
module graph always needs a fresh run. To iterate on a detached run stuck at a
gate, keep a live `up --hot` process attached, or start a new run and gate
finished phases off with an input flag to skip them.
### Never pin `cwd` on an agent you use inside ``
A pinned `cwd` takes precedence over the per-task root: the agent reads,
writes, and commits to the launch directory's base branch instead of its
worktree. Leave `cwd` unset and let `` (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
Both rewrite filesystem state; treat them like `git checkout` over uncommitted work.
- `revert` restores the workspace to a previous attempt's filesystem state, discarding graph snapshots recorded after it; it touches files only, landing as a new change atop 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 original run's source.
### `revert` requires jj
Smithers prefers `.jj` over `.git`: pure Git repos run fine but can't use `revert`, since there's no per-attempt change to restore. Install jj for attempt-level revert. See VCS.
### Worktree runs auto-rebase on resume
On resume, Smithers rebases a worktree run onto the base branch (default `main`), continuing even if the rebase fails: expect the branch to move.
### `` must be a stable branch, not the current change
`baseBranch` defaults to `main` and should name a committed branch or described commit, not the current working-copy commit (e.g. `jj log -r @` output): a jj `@` snapshots uncommitted launcher changes into an undescribed commit, so the worktree inherits that dirty tree, with a branch based on a commit jj refuses to push ("Won't push commit ... since it has no description"). Omit `baseBranch` for `main`, or name an explicit clean branch.
## Outputs
### `ctx.outputMaybe` is undefined until the task runs
Reading a downstream output before its task completes returns `undefined`, not a default: guard it so a not-yet-run task doesn't crash the render.
```tsx
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
return analysis ? ... : null;
```
When a task only waits on one upstream output, `` with a `(deps) => ...` callback is more direct: 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.` pools every row written for that schema, across every
node and loop iteration. Fan out, then filter on a discriminator field for
"the latest row for item X": any other task writing to that schema lands in
the same pool, so a setup or sentinel task reusing a filtered schema (say a
placeholder `validation` row from a `prepare` step) can be mistaken for real
per-item state. Give unrelated streams their own schema, and match on a
discriminator set on every row.
```tsx
// prepare gets its OWN schema, not the validation schema the loop filters by deliverable
{() => ({ specFound: true })}
```
## 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.
This mirrors what `tsc --emitDeclarationOnly` would emit. 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 the workflow module. Destructure what you
need (`Workflow`, `Task`, `Branch`, `Sandbox`, `smithers`, `outputs`) or use
property access (`api.Workflow`, `api.Task`); both work for one factory, but
property access reads clearer with multiple factories, e.g. parent/child
workflows.
There is no top-level default `smithers` export: the `smithers` wrapper is the
property returned by `createSmithers(...)`. `smithers((ctx) => )`
returns the `SmithersWorkflow` value that workflow files usually export as
their default.
`input` is a reserved schema key on `createSmithers(...)` that controls the
TypeScript type of `ctx.input`; every other key is an output schema and becomes
a typed output ref under `outputs.`. Fresh runs and graph previews parse
input through this schema before rendering, so Zod defaults and transforms are
available in `ctx.input`. The parsed fresh-run input is persisted; previews do
not persist or mutate input. Treat required Zod fields as the authored
TypeScript contract. For fields intentionally declared `.optional()` or
`.nullable()`, coalesce inside the workflow (`ctx.input.dryRun ?? false`).
Prefer `output={outputs.someKey}` on tasks, approvals, sandboxes, and subflows:
the `outputs` object holds the exact Zod schema objects passed to
`createSmithers(...)`, so the task can infer `outputSchema`, validate and
persist the returned row to the matching table, and feed the same schema to
native structured-output agents.
Major sections at a glance:
- **Workflow / Context**: `SmithersWorkflow`, `WorkflowFileRef`, `SmithersCtx`, `RunOptions`, `RunResult`: entry points for defining and running workflows.
- **Task / Graph**: `TaskDescriptor`, `TaskProps`, `GraphSnapshot`: node shape 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.
- **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 {
readonly readableName?: string;
readonly description?: string;
readonly ui?: WorkflowViewDefinition;
readonly tui?: WorkflowViewDefinition;
readonly db?: unknown;
readonly build: (ctx: SmithersCtx) => JSX.Element;
readonly opts: SmithersWorkflowOptions;
readonly memoryService?: import("@smithers-orchestrator/driver/MemoryRuntimeService").MemoryRuntimeService;
readonly schemaRegistry?: Map;
readonly zodToKeyName?: Map, string>;
}
type WorkflowFileRef = {
path: string;
approvedRoot?: string;
};
type WorkflowViewDefinition = {
kind: "ui" | "tui";
title?: string;
props?: Record;
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 's child result).
// Defaults to the schema key literally named `output`.
output?: SmithersWorkflowOutputTarget;
workflowHash?: string;
};
// A Zod schema (createSmithers(...).outputs.), 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;
type SchemaRegistryEntry = {
table: any;
zodSchema: import("zod").ZodObject;
};
type SmithersAlertPolicy = {
defaults?: SmithersAlertPolicyDefaults;
rules?: Record;
reactions?: Record;
};
type SmithersAlertSeverity = "info" | "warning" | "critical";
type SmithersAlertLabels = Record;
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 {
runId: string;
iteration: number;
iterations: Record | undefined;
input: Schema extends { input: infer T } ? T : unknown;
auth: RunAuthContext | null;
outputs: OutputAccessor;
outputRows: (output: any, options?: { nodeId?: string; scope?: string }) => Array<{
payload: unknown;
nodeId: string;
iteration: number;
seq: number;
}>;
// 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;
prove(table: any, key: OutputKey): ProofBinding | undefined;
boundStale(nodeId: string): boolean;
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 ProofBinding = {
table: string;
nodeId: string;
iteration: number;
digest: string; // sha256: over canonical JSON row content
};
type SafeParser = {
safeParse(value: unknown):
| { success: true; data: unknown }
| { success: false; error?: unknown };
};
type InferRow = TTable extends { $inferSelect: infer R } ? R : never;
type InferOutputEntry =
T extends import("zod").ZodTypeAny ? import("zod").infer
: T extends { $inferSelect: any } ? InferRow
: never;
type FallbackTableName = [keyof Schema & string] extends [never] ? string : never;
type OutputAccessor = {
(table: FallbackTableName): Array;
(table: K): Array>;
} & {
[K in keyof Schema & string]: Array>;
};
type OutputSnapshot = {
[tableName: string]: Array;
};
type RunAuthContext = {
triggeredBy: string;
scopes: string[];
role: string;
createdAt: string;
};
```
## Status contracts
`RunStatus` is the persisted run lifecycle and the type of
`RunResult.status`. Its categories are:
| Category | Values | Meaning |
|---|---|---|
| Active | `running` | Work can be executing or scheduled. |
| Suspended | `waiting-approval`, `waiting-event`, `waiting-timer`, `waiting-quota`, `paused` | The run is nonterminal and can resume. |
| Terminal | `finished`, `continued`, `failed`, `cancelled` | This run will not schedule more work. A `continued` run points to its successor through `nextRunId`. |
`TaskState` is the persisted, detailed lifecycle for an individual task.
Import it from `@smithers-orchestrator/scheduler/TaskState`. `NodeStatus` is a
separate six-value display palette exported by
`@smithers-orchestrator/gateway-react`; `useGatewayRunTree` collapses detailed
task and run states into that palette. In particular, every `waiting-*` value
maps to `waiting`. Do not use the display palette as a persisted lifecycle
contract.
| Task category | Values |
|---|---|
| Queued | `pending` |
| Active | `in-progress` |
| Suspended | `waiting-approval`, `waiting-event`, `waiting-timer`, `waiting-quota`, `waiting-bound`, `bound-stale` |
| Terminal | `finished`, `failed`, `cancelled`, `skipped` |
Status unions can gain values. Add a visible unknown fallback when consuming
Gateway or event data, preserve the original value for diagnostics, and treat
an unknown `waiting-*` value as suspended and nonterminal. See the
compatibility and changelog policy.
```ts
// =============================================================================
// Run
// =============================================================================
type SmithersErrorReport = {
readonly error: SmithersError;
readonly rawError: unknown;
readonly runId: string;
} & (
| {
readonly phase: "run";
readonly nodeId?: undefined;
readonly iteration?: undefined;
readonly attempt?: undefined;
}
| {
readonly phase: "node";
readonly nodeId: string;
readonly iteration: number;
readonly attempt: number;
}
);
type RunOptions = {
runId?: string;
parentRunId?: string | null;
input: Record;
maxConcurrency?: number; // default 4
maxConcurrencyPinned?: boolean; // internal/runtime proof that the value was explicitly persisted
requireRerenderOnOutputChange?: boolean; // default true; re-render the frame on every task completion
onProgress?: (e: SmithersEvent) => void;
onError?: (report: SmithersErrorReport) => void; // once per NodeFailed or RunFailed occurrence
signal?: AbortSignal;
pauseSignal?: AbortSignal; // graceful pause: stop scheduling, let in-flight finish, park `paused`
resume?: boolean;
force?: boolean; // resume even if marked running
acceptWorkflowChange?: boolean; // resume same run id after workflow source changed, re-blessing hashes
workflowPath?: string;
rootDir?: string;
keepWorktrees?: boolean; // default false; keep this run's dirs instead of reaping on success (SMITHERS_KEEP_WORKTREES=1 process-wide)
logDir?: string | null;
allowNetwork?: boolean; // default false; bash tool egress (loopback always allowed)
maxOutputBytes?: number; // default 200000
toolTimeoutMs?: number; // default 60000
hot?: boolean | HotReloadOptions;
annotations?: Record;
auth?: RunAuthContext | null;
startedBy?: RunStartedBy; // optional self-reported launch provenance
config?: Record;
effectPlatformRuntime?: "bun" | "node" | "worker"; // swappable @effect/platform layer; "node"/"worker" require effectPlatformLayer
effectPlatformLayer?: Layer.Layer; // e.g. NodeContext.layer from a Node serverless entrypoint
cliAgentToolsDefault?: "all" | "explicit-only"; // default "all"
initialOutputs?: OutputSnapshot; // seed prior outputs (resume/fork)
signals?: SignalRowInput[]; // fallback signal rows when the runtime adapter has no durable signal capability
initialIteration?: number; // seed the starting loop iteration
initialIterations?: Record | ReadonlyMap; // per-loop iteration seeds
resumeClaim?: { // internal supervisor coordination
claimOwnerId: string;
claimHeartbeatAtMs: number;
restoreRuntimeOwnerId?: string | null;
restoreHeartbeatAtMs?: number | null;
};
};
type RunStartedBy = {
harness?: string; // trimmed, at most 64 Unicode code points
sessionId?: string; // trimmed, at most 256 Unicode code points
prompt?: string; // explicit-only; visibly clipped to 8,192 Unicode code points
detected?: true; // only when environment inference filled harness/session
};
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"
| "paused"
| "finished"
| "continued"
| "failed"
| "cancelled";
// Persisted per-task lifecycle. Import from
// @smithers-orchestrator/scheduler/TaskState.
type TaskState =
| "pending"
| "waiting-approval"
| "waiting-event"
| "waiting-timer"
| "waiting-quota"
| "waiting-bound"
| "bound-stale"
| "in-progress"
| "finished"
| "failed"
| "cancelled"
| "skipped";
// Display palette returned by useGatewayRunTree. Import from
// @smithers-orchestrator/gateway-react.
type NodeStatus =
| "ok"
| "running"
| "queued"
| "failed"
| "waiting"
| "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;
proofBindingRequired?: boolean;
proofBindings?: readonly ProofBinding[];
proofBindingStatus?: "current" | "missing" | "stale";
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;
outputSchema?: import("zod").ZodObject;
parallelGroupId?: string;
parallelMaxConcurrency?: number;
subtreeGroupId?: string; // nearest ancestor 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;
label?: string;
meta?: Record;
scorers?: ScorersMap;
memoryConfig?: TaskMemoryConfig;
};
type RetryPolicy = {
backoff?: "fixed" | "linear" | "exponential"; // default "fixed"
initialDelayMs?: number; // default 0
};
type CachePolicy = {
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" | "cursor" | "antigravity" | "gemini" | "kimi" | "pi" | "omp" | "amp" | "forge" | "hermes" | "opencode" | "openclaw" | "pool" | "vibe";
runtimeTools: Record;
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;
supportsNativeStructuredOutput?: boolean;
capabilities?: AgentCapabilityRegistry;
generate: (args?: AgentGenerateOptions) => Promise;
};
type SdkAgentOptions =
Omit, "model"> & {
model: string | MODEL;
};
type AnthropicAgentOptions =
SdkAgentOptions;
type OpenAIAgentOptions =
Omit, "model"> & {
nativeStructuredOutput?: boolean;
} & (
| { model: string; baseURL?: string; apiKey?: string; api?: "responses" | "chat" }
| { model: import("ai").LanguageModel; baseURL?: never; apiKey?: never; api?: never }
);
type HermesAgentOptions =
Omit, "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;
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;
};
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 = {
bank?: string;
banks?: string[];
tags?: string[];
recall?:
| "auto"
| string
| false
// Legacy object form: preserved but inert.
| { namespace?: MemoryNamespace; query?: string; topK?: number };
budget?: "low" | "mid" | "high";
maxTokens?: number;
primers?: string[];
retain?: "on-complete" | "off";
tools?: boolean;
namespace?: string | MemoryNamespace; // Legacy: preserved but inert.
remember?: { namespace?: MemoryNamespace; key?: string }; // Legacy: inert.
threadId?: string; // Legacy: preserved but inert.
};
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;
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