# 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) ``` A Smithers run is killed partway through, then resumes: the completed task is skipped, the in-flight task re-runs as a new attempt, and the run finishes 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. A four-stage loop: render the workflow tree, extract ready tasks, execute them, persist outputs to SQLite, then re-render against the new state 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 Unidirectional data flow: action events update state, state maps forward into the execution plan, and the plan registers the next action handlers 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 ``` A Smithers run crashes partway through, then resumes: the finished task is skipped, the in-flight task re-runs as a new attempt, and the remaining tasks execute 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; // (Each variant has additional fields per its `type`. See event-types.) // ============================================================================= // Component props // ============================================================================= type WorkflowProps = { name: string; cache?: boolean; children?: React.ReactNode; }; // OutputTarget accepts a Zod output schema (recommended, usually outputs.key), // a custom Drizzle table object, or a string schema key escape hatch. type OutputTarget = import("zod").ZodObject | { $inferSelect: any } | string; type DepsSpec = Record; type InferDeps = { [K in keyof D]: D[K] extends string ? unknown : InferOutputEntry; }; type TaskProps = { key?: string; id: string; output: Output; outputSchema?: import("zod").ZodObject; maxSchemaRetries?: number; // default 3; correction calls after the initial agent response agent?: AgentLike | AgentLike[]; fallbackAgent?: AgentLike; dependsOn?: string[]; needs?: Record; deps?: D; depsOptional?: boolean; fork?: string; // start from another task's final agent session snapshot bind?: ProofBinding | ProofBinding[]; skipIf?: boolean; needsApproval?: boolean; async?: boolean; // only with needsApproval timeoutMs?: number; heartbeatTimeoutMs?: number; heartbeatTimeout?: number; // alias for heartbeatTimeoutMs noRetry?: boolean; retries?: number; // default Infinity (set 0 to disable) retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; scorers?: ScorersMap; groundTruth?: unknown; context?: unknown; memory?: TaskMemoryConfig; hijack?: boolean; onHijackExit?: "complete" | "reopen"; allowTools?: string[]; // CLI-agent tool allowlist sideEffect?: boolean | { idempotent?: boolean; revert?: (ctx: { outputRow: unknown | null; effectStatus: "succeeded" | "unknown"; runId: string; nodeId: string; iteration: number; attempt: number; }) => Promise; }; priority?: number; failurePolicy?: "halt" | "quarantine"; label?: string; meta?: Record; // string = prompt literal; Row = static result; () => Row = compute fn; // (deps) => result = deps-aware fn; React.ReactNode = JSX subtree children: string | Row | (() => Row | Promise) | React.ReactNode | ((deps: InferDeps) => Row | Promise | React.ReactNode); }; type SequenceProps = { key?: string; label?: string; failurePolicy?: "halt" | "quarantine"; skipIf?: boolean; children?: React.ReactNode; }; type ParallelProps = { id?: string; label?: string; maxConcurrency?: number; subtreeConcurrency?: number; priority?: number; failurePolicy?: "halt" | "quarantine"; skipIf?: boolean; children?: React.ReactNode; }; type MemoryProps = { bank?: string; banks?: string[]; tags?: string[]; recall?: "auto" | string | false; budget?: "low" | "mid" | "high"; maxTokens?: number; primers?: string[]; retain?: "on-complete" | "off"; tools?: boolean; children?: React.ReactNode; }; type MonitorProps = { id?: string; watchRunId: string; watchWorkflowPath?: string; agent: AgentLike | AgentLike[]; healthOutput: OutputTarget; actionOutput?: OutputTarget; healAgent?: AgentLike | AgentLike[]; intervalMs?: number; maxChecks?: number; stallBeats?: number; autoHeal?: MonitorCondition[]; handlers?: Partial>; guidance?: string; prompt?: string | React.ReactNode; skipIf?: boolean; children?: string | React.ReactNode; }; type BranchProps = { if: boolean; then: React.ReactElement; else?: React.ReactElement | null; skipIf?: boolean }; type LoopProps = { key?: string; id?: string; until?: boolean; maxIterations?: number; onMaxReached?: "fail" | "return-last"; // default "return-last" continueAsNewEvery?: number; skipIf?: boolean; children?: React.ReactNode; }; type RalphProps = LoopProps; // deprecated alias type ApprovalDecision = { approved: boolean; note: string | null; decidedBy: string | null; decidedAt: string | null }; type ApprovalSelection = { selected: string; notes: string | null }; type ApprovalRanking = { ranked: string[]; notes: string | null }; type ApprovalRequest = { title: string; summary?: string; metadata?: Record }; type ApprovalMode = "approve" | "select" | "rank"; type ApprovalOption = { key: string; label: string; summary?: string; metadata?: Record }; type ApprovalAutoApprove = { after?: number; condition?: ((ctx: SmithersCtx | null) => boolean) | (() => boolean); audit?: boolean; revertOn?: ((ctx: SmithersCtx | null) => boolean) | (() => boolean); }; type ApprovalProps = { id: string; mode?: ApprovalMode; options?: ApprovalOption[]; output: Output; outputSchema?: import("zod").ZodObject; request: ApprovalRequest; onDeny?: "fail" | "continue" | "skip"; allowedScopes?: string[]; allowedUsers?: string[]; autoApprove?: ApprovalAutoApprove; async?: boolean; dependsOn?: string[]; needs?: Record; skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; heartbeatTimeout?: number; // alias for heartbeatTimeoutMs retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; label?: string; meta?: Record; key?: string; children?: React.ReactNode; }; type SignalProps = import("zod").ZodObject> = { id: string; schema: S; correlationId?: string; timeoutMs?: number; onTimeout?: "fail" | "skip" | "continue"; async?: boolean; skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; children?: (data: import("zod").infer) => React.ReactNode; }; type WaitForEventProps = { id: string; event: string; correlationId?: string; output: OutputTarget; outputSchema?: import("zod").ZodObject; timeoutMs?: number; onTimeout?: "fail" | "skip" | "continue"; tagged?: boolean; async?: boolean; skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; }; type TimerProps = { id: string; duration?: string; // e.g. "30s", "5m" until?: string | Date; // absolute timestamp every?: string; // reserved; recurring timers are not supported yet skipIf?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; }; type MonitorCondition = | "healthy" | "stalled" | "wedged-node" | "runaway-loop" | "awaiting-human" | "failing" | "unknown"; type MonitorProps = { id?: string; // id prefix for generated nodes; default "monitor" watchRunId: string; // the run this monitor watches watchWorkflowPath?: string; // workflow path used by safe resume/retry commands agent: AgentLike | AgentLike[]; // samples the watched run and classifies its health healthOutput: OutputTarget; // schema must carry `condition` + `runStatus` actionOutput?: OutputTarget; // handler tasks write here; defaults to healthOutput healAgent?: AgentLike | AgentLike[]; intervalMs?: number; // heartbeat spacing; default 60000 maxChecks?: number; // heartbeats before the monitor stops; default 120 stallBeats?: number; // beats without progress before `stalled`; default 3 autoHeal?: MonitorCondition[]; // healed without a human; default ["stalled", "wedged-node"] handlers?: Partial>; guidance?: string; // extra doctrine appended to the shipped prompt prompt?: string | React.ReactNode; skipIf?: boolean; children?: string | React.ReactNode; }; type SagaStepDef = { id: string; action: React.ReactElement; compensation: React.ReactElement; label?: string }; type SagaProps = { id?: string; steps?: SagaStepDef[]; onFailure?: "compensate" | "compensate-and-fail" | "fail"; skipIf?: boolean; children?: React.ReactNode }; type SagaStepProps = { id: string; compensation: React.ReactElement; children: React.ReactElement }; type TryCatchFinallyProps = { id?: string; try: React.ReactElement; catch?: React.ReactElement | ((error: SmithersError) => React.ReactElement); catchErrors?: SmithersErrorCode[]; finally?: React.ReactElement; skipIf?: boolean; }; // Higher-level composites type PollerProps = { id?: string; check: AgentLike | (() => unknown | Promise); checkOutput: OutputTarget; maxAttempts?: number; backoff?: "fixed" | "linear" | "exponential"; intervalMs?: number; checkTimeoutMs?: number; onTimeout?: "fail" | "return-last"; skipIf?: boolean; children?: React.ReactNode; }; type ColumnTaskProps = Omit>, "agent" | "children" | "id" | "key" | "output" | "smithersContext">; type ColumnDef = { name: string; agent: AgentLike; output: OutputTarget; prompt?: (ctx: { item: unknown; column: string }) => string; task?: ColumnTaskProps; }; type KanbanProps = { id?: string; columns: ColumnDef[]; useTickets: () => Array<{ id: string; [key: string]: unknown }>; agents?: Record; maxConcurrency?: number; onComplete?: OutputTarget; until?: boolean; maxIterations?: number; skipIf?: boolean; children?: React.ReactNode | Record; }; type ApprovalGateProps = { id: string; output: OutputTarget; request: ApprovalRequest; when: boolean; // false auto-approves onDeny?: "fail" | "continue" | "skip"; skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; heartbeatTimeout?: number; retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; }; type HumanTaskProps = { id: string; output: OutputTarget; outputSchema?: import("zod").ZodObject; prompt: string | React.ReactNode; maxAttempts?: number; async?: boolean; skipIf?: boolean; timeoutMs?: number; continueOnFail?: boolean; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; }; type CheckConfig = { id: string; agent?: AgentLike; command?: string; label?: string; timeoutMs?: number; }; type CheckSuiteProps = { id?: string; checks: CheckConfig[] | Record>; verdictOutput: OutputTarget; strategy?: "all-pass" | "majority" | "any-pass"; maxConcurrency?: number; continueOnFail?: boolean; skipIf?: boolean; }; type TokenBudgetConfig = { max: number; perTask?: number; onExceeded?: "fail" | "warn" | "skip-remaining" }; type LatencySloConfig = { maxMs: number; perTask?: number; onExceeded?: "fail" | "warn" }; type TrackingConfig = { tokens?: boolean; latency?: boolean }; type AspectsProps = { tokenBudget?: TokenBudgetConfig; latencySlo?: LatencySloConfig; tracking?: TrackingConfig; children?: React.ReactNode; }; type CategoryConfig = { agent: AgentLike; output?: OutputTarget; prompt?: (item: unknown) => string; }; type ClassifyAndRouteProps = { id?: string; items: unknown | unknown[]; categories: Record; classifierAgent: AgentLike; classifierOutput: OutputTarget; routeOutput: OutputTarget; classificationResult?: { classifications: Array<{ itemId?: string; category: string; [key: string]: unknown }>; } | null; maxConcurrency?: number; skipIf?: boolean; children?: React.ReactNode; }; type SourceDef = { agent: AgentLike; prompt?: string; output?: OutputTarget; children?: React.ReactNode; }; type GatherAndSynthesizeProps = { id?: string; sources: Record; synthesizer: AgentLike; gatherOutput: OutputTarget; synthesisOutput: OutputTarget; gatheredResults?: Record | null; maxConcurrency?: number; synthesisPrompt?: string; skipIf?: boolean; children?: React.ReactNode; }; type ContentPipelineStage = { id: string; agent: AgentLike; output: OutputTarget; label?: string }; type ContentPipelineProps = { id?: string; stages: ContentPipelineStage[]; skipIf?: boolean; children: string | React.ReactNode; }; type DebateProps = { id?: string; proposer: AgentLike; opponent: AgentLike; judge: AgentLike; rounds?: number; argumentOutput: OutputTarget; verdictOutput: OutputTarget; topic: string | React.ReactNode; skipIf?: boolean; }; type DecisionRule = { when: boolean; then: React.ReactElement; label?: string }; type DecisionTableProps = { id?: string; rules: DecisionRule[]; default?: React.ReactElement; strategy?: "first-match" | "all-match"; skipIf?: boolean; }; type DriftDetectorProps = { id?: string; captureAgent: AgentLike; compareAgent: AgentLike; captureOutput: OutputTarget; compareOutput: OutputTarget; baseline: unknown; alertIf?: (comparison: unknown) => boolean; alert?: React.ReactElement; poll?: { intervalMs?: number; maxPolls?: number }; skipIf?: boolean; }; type EscalationLevel = { agent: AgentLike; output: OutputTarget; label?: string; escalateIf?: (result: unknown) => boolean; }; type EscalationChainProps = { id?: string; levels: EscalationLevel[]; humanFallback?: boolean; humanRequest?: ApprovalRequest; escalationOutput: OutputTarget; skipIf?: boolean; children?: React.ReactNode; }; type MergeQueueProps = { id?: string; maxConcurrency?: number; priority?: number; failurePolicy?: "halt" | "quarantine"; skipIf?: boolean; children?: React.ReactNode }; type Tier = "fable" | "opus" | "sonnet" | "haiku"; type DelegationAgents = Partial>; type DelegationOutputs = { dcGoal: OutputTarget; dcQuestion: OutputTarget; dcForecast: OutputTarget; dcGoalApproval: OutputTarget; dcPlan: OutputTarget; dcPreview: OutputTarget; dcDevPreview?: OutputTarget; dcGates: OutputTarget; dcProbe: OutputTarget; dcReplan: OutputTarget; dcExec: OutputTarget; dcReview: OutputTarget; dcApproval?: OutputTarget; dcEdit: OutputTarget; dcSkip: OutputTarget; dcPoll: OutputTarget; dcBudget?: OutputTarget; dcScore?: OutputTarget; }; type DelegationBudget = { maxUsd?: number; maxMinutes?: number }; type DelegationScorers = { exec?: ScorersMap; review?: ScorersMap; run?: ScorersMap; }; type DelegationSharedProps = { idPrefix?: string; agents: DelegationAgents; outputs: DelegationOutputs; approvalPolicy?: string; tierOrder?: Tier[]; maxDepth?: number; maxConcurrency?: number; maxDeriskRounds?: number; poll?: boolean; budget?: DelegationBudget; scorers?: DelegationScorers; skipIf?: boolean; }; type GoalRefinementProps = DelegationSharedProps & { prompt: string; maxQuestions?: number; prefetchDepth?: number; }; type DelegationPlanningProps = DelegationSharedProps & { prompt?: string }; type DelegationPreviewProps = DelegationSharedProps; type BackpressurePlanningProps = DelegationSharedProps; type DeriskLoopProps = DelegationSharedProps; type DelegationExecutionProps = DelegationSharedProps & { maxAttempts?: number }; type DelegationScoringProps = DelegationSharedProps; type DelegationEditListenerProps = DelegationSharedProps & { until?: boolean; maxEdits?: number; }; type DelegationChainProps = DelegationSharedProps & { prompt: string; maxQuestions?: number; prefetchDepth?: number; maxAttempts?: number; maxEdits?: number; }; type DelegationV2Role = "sol" | "fable" | "terra" | "luna"; type DelegationV2WorkKind = | "refine_goal" | "plan" | "research" | "poc" | "execute" | "review" | "preview" | "synthesize"; type OutputContractId = | "work_product" | "goal_contract" | "plan" | "evaluation" | "classification" | "issue_scan" | "condition" | "artifact_collection" | "evidence_collection"; type GoalContract = { objective: string; context: string[]; constraints: string[]; nonGoals: string[]; }; type AcceptanceCriterion = { id: string; requirement: string; verification: string; }; type TrellisCriticalExecutionCategory = | "security_boundary" | "data_integrity" | "concurrency_invariant" | "protocol_core" | "irreversible_migration"; type TrellisAgents = Record<"sol" | "fable" | "terra" | "luna", AgentLike | AgentLike[]>; type TrellisOutputs = { dv2Author: OutputTarget; dv2Worker: OutputTarget; dv2Validation: OutputTarget; dv2Outcome: OutputTarget; dv2Final: OutputTarget; dv2Question: OutputTarget; dv2Answer: OutputTarget; }; type TrellisLimits = { maxTotalAuthorTurns?: number; maxAuthorGenerations?: number; maxAuthorDepth?: number; maxNodesPerProgram?: number; maxProgramDepth?: number; maxFanout?: number; maxPromptBytes?: number; maxTotalPromptBytes?: number; }; type TrellisCriticalExecutionPolicy = { allowedCategories: TrellisCriticalExecutionCategory[]; allowedPathPrefixes: string[]; maxChangedLines: number; }; type TrellisProps = { prompt: string; goal?: GoalContract; acceptance?: AcceptanceCriterion[]; instructions?: string; role?: "sol" | "fable"; work?: DelegationV2WorkKind; outputContract?: OutputContractId; agents: TrellisAgents; outputs: TrellisOutputs; idPrefix?: string; semanticRevision?: string; criticalExecutionPolicy?: TrellisCriticalExecutionPolicy; maxConcurrency?: number; limits?: TrellisLimits; skipIf?: boolean; }; type OptimizerProps = { id?: string; generator: AgentLike; evaluator: AgentLike | ((candidate: unknown) => unknown | Promise); generateOutput: OutputTarget; evaluateOutput: OutputTarget; targetScore?: number; maxIterations?: number; onMaxReached?: "return-last" | "fail"; skipIf?: boolean; children: string | React.ReactNode; }; type PanelistConfig = { agent: AgentLike | AgentLike[]; role?: string; label?: string }; type PanelProps = { id?: string; // Each entry is an agent, a PanelistConfig, or a failover chain (AgentLike[]); // a chain becomes one panelist run as failover. panelists: Array; // the synthesizing moderator; a chain (AgentLike[]) runs as failover. moderator: AgentLike | AgentLike[]; panelistOutput: OutputTarget; moderatorOutput: OutputTarget; strategy?: "synthesize" | "vote" | "consensus"; minAgree?: number; maxConcurrency?: number; // extra Task props for each panelist / the moderator (continueOnFail, timeouts) panelistTaskProps?: { continueOnFail?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number }; moderatorTaskProps?: { continueOnFail?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; retries?: number }; skipIf?: boolean; children: string | React.ReactNode; }; type SidecarProps = { id?: string; agent: AgentLike; sidecar: AgentLike; output: OutputTarget; sidecarOutput?: OutputTarget; scorers?: ScorersMap; prompt?: string | React.ReactNode; input?: string | React.ReactNode; maxConcurrency?: number; groundTruth?: unknown; context?: unknown; primaryLabel?: string; sidecarLabel?: string; skipIf?: boolean; children?: string | React.ReactNode; }; type SidecarDelta = { primaryScore: number | null; sidecarScore: number | null; delta: number | null; cheaperWins: boolean; }; type ReviewLoopProps = { id?: string; producer: AgentLike; reviewer: AgentLike | AgentLike[]; produceOutput: OutputTarget; reviewOutput: OutputTarget; maxIterations?: number; onMaxReached?: "return-last" | "fail"; skipIf?: boolean; children: string | React.ReactNode; }; type RunbookStep = { id: string; agent?: AgentLike; command?: string; risk: "safe" | "risky" | "critical"; label?: string; output?: OutputTarget; }; type RunbookProps = { id?: string; steps: RunbookStep[]; defaultAgent?: AgentLike; stepOutput: OutputTarget; approvalRequest?: Partial; onDeny?: "fail" | "skip"; skipIf?: boolean; }; type ScanFixVerifyProps = { id?: string; scanner: AgentLike; fixer: AgentLike | AgentLike[]; verifier: AgentLike; scanOutput: OutputTarget; fixOutput: OutputTarget; verifyOutput: OutputTarget; reportOutput: OutputTarget; maxConcurrency?: number; maxRetries?: number; skipIf?: boolean; children?: React.ReactNode; }; type SupervisorProps = { id?: string; boss: AgentLike; workers: Record; planOutput: OutputTarget; workerOutput: OutputTarget; reviewOutput: OutputTarget; finalOutput: OutputTarget; maxIterations?: number; maxConcurrency?: number; useWorktrees?: boolean; skipIf?: boolean; children: string | React.ReactNode; }; type MonitorCondition = | "healthy" | "stalled" | "wedged-node" | "runaway-loop" | "awaiting-human" | "failing" | "unknown"; type MonitorProps = { id?: string; watchRunId: string; watchWorkflowPath?: string; agent: AgentLike | AgentLike[]; healthOutput: OutputTarget; actionOutput?: OutputTarget; healAgent?: AgentLike | AgentLike[]; intervalMs?: number; maxChecks?: number; stallBeats?: number; autoHeal?: MonitorCondition[]; handlers?: Partial>; guidance?: string; prompt?: string | React.ReactNode; skipIf?: boolean; children?: string | React.ReactNode; }; type ContinueAsNewProps = { state?: unknown }; // Sandbox type SandboxRuntime = "bubblewrap" | "docker" | "codeplane" | "cloudflare"; type SandboxEgressConfig = { env?: Record; httpProxy?: string; httpsProxy?: string; noProxy?: string | string[]; caCertPem?: string; caCertPath?: string; secretBindings?: Record; }; type SandboxVolumeMount = { host: string; container: string; readonly?: boolean }; type SandboxWorkspaceSpec = { name: string; snapshotId?: string; idleTimeoutSecs?: number; persistence?: "ephemeral" | "sticky"; }; type SandboxWorkflow = { db?: unknown; build: (ctx: unknown) => unknown; opts?: Record; schemaRegistry?: unknown; zodToKeyName?: unknown; }; type SandboxChildWorkflowDefinition = | SandboxWorkflow | (() => SandboxWorkflow | unknown); type ExecuteSandboxChildWorkflowOptions = { workflow: SandboxChildWorkflowDefinition; input?: unknown; runId?: string; parentRunId?: string; rootDir?: string; allowNetwork?: boolean; maxOutputBytes?: number; toolTimeoutMs?: number; workflowPath?: string; signal?: AbortSignal; }; type ExecuteSandboxChildWorkflow = ( parentWorkflow: SandboxWorkflow | undefined, options: ExecuteSandboxChildWorkflowOptions, ) => Promise<{ runId: string; status: string; output: unknown }>; type SandboxDiffBundleLike = { seq: number; baseRef: string; patches: Array<{ path: string; operation: "add" | "modify" | "delete"; diff: string; binaryContent?: string; }>; }; type SandboxProviderRequest = { runId: string; sandboxId: string; input?: unknown; rootDir: string; requestBundlePath: string; resultBundlePath: string; workflow: SandboxChildWorkflowDefinition; parentWorkflow?: SandboxWorkflow; executeChildWorkflow: ExecuteSandboxChildWorkflow; allowNetwork: boolean; maxOutputBytes: number; toolTimeoutMs: number; egress?: SandboxEgressConfig; config: Record; signal?: AbortSignal; heartbeat: (data?: unknown) => void; }; type SandboxProviderResult = | { bundlePath: string; remoteRunId?: string; workspaceId?: string; containerId?: string } | { status: "finished" | "failed" | "cancelled"; output?: unknown; outputs?: unknown; runId?: string; remoteRunId?: string; workspaceId?: string; containerId?: string; diffBundle?: SandboxDiffBundleLike; patches?: Array<{ path: string; content: string }>; artifacts?: Array<{ path: string; content: string }>; streamLogPath?: string | null; }; type SandboxProvider = { id: string; run(request: SandboxProviderRequest): Promise | SandboxProviderResult; cleanup?(request: SandboxProviderRequest): Promise | void; }; type ExecuteSandboxOptions = { parentWorkflow?: SandboxWorkflow; sandboxId: string; provider?: SandboxProvider | string; runtime?: SandboxRuntime; workflow: SandboxChildWorkflowDefinition; executeChildWorkflow: ExecuteSandboxChildWorkflow; applyDiffBundle?: (bundle: SandboxDiffBundleLike, targetDir: string) => Promise; input?: unknown; rootDir: string; allowNetwork: boolean; maxOutputBytes: number; toolTimeoutMs: number; reviewDiffs?: boolean; autoAcceptDiffs?: boolean; allowNested?: boolean; config?: Record; }; type SandboxProps = { id: string; workflow?: SmithersWorkflow; input?: unknown; output: OutputTarget; provider?: unknown; // runtime accepts a provider object or registered provider id runtime?: SandboxRuntime; // legacy local transports allowNetwork?: boolean; reviewDiffs?: boolean; autoAcceptDiffs?: boolean; allowNested?: boolean; image?: string; env?: Record; egress?: SandboxEgressConfig; ports?: Array<{ host: number; container: number }>; volumes?: SandboxVolumeMount[]; memoryLimit?: string; cpuLimit?: string; command?: string; workspace?: SandboxWorkspaceSpec; skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; heartbeatTimeout?: number; // alias for heartbeatTimeoutMs retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; children?: React.ReactNode; }; type SubflowProps = { id: string; workflow: SmithersWorkflow; input?: unknown; mode?: "childRun" | "inline"; output: OutputTarget; skipIf?: boolean; timeoutMs?: number; heartbeatTimeoutMs?: number; heartbeatTimeout?: number; // alias for heartbeatTimeoutMs retries?: number; retryPolicy?: RetryPolicy; continueOnFail?: boolean; cache?: CachePolicy; dependsOn?: string[]; needs?: Record; label?: string; meta?: Record; key?: string; children?: React.ReactNode; }; type WorktreeProps = { key?: string; id?: string; path: string; branch?: string; baseBranch?: string; // default "main" skipIf?: boolean; children?: React.ReactNode; }; type SuperSmithersProps = { id?: string; // default "super-smithers"; prefixes internal task ids strategy: string | React.ReactElement; agent: AgentLike; targetFiles?: string[]; reportOutput?: OutputTarget; dryRun?: boolean; // default false skipIf?: boolean; }; // ============================================================================= // Errors // ============================================================================= // // Every Smithers error is a SmithersError with a typed code. See the Errors page // for the full list of built-in codes. declare class SmithersError extends Error { readonly code: SmithersErrorCode; readonly summary: string; readonly docsUrl: string; readonly details?: Record; readonly cause?: unknown; } type SmithersErrorCode = KnownSmithersErrorCode | (string & {}); type KnownSmithersErrorCode = | "INVALID_INPUT" | "MISSING_INPUT" | "MISSING_INPUT_TABLE" | "RESUME_METADATA_MISMATCH" | "UNKNOWN_OUTPUT_SCHEMA" | "INVALID_OUTPUT" | "WORKTREE_CREATE_FAILED" | "VCS_NOT_FOUND" | "SNAPSHOT_NOT_FOUND" | "VCS_WORKSPACE_CREATE_FAILED" | "TASK_TIMEOUT" | "TASK_HIJACK_UNSUPPORTED" | "TASK_FORK_SOURCE_NOT_FOUND" | "TASK_FORK_SOURCE_NOT_COMPLETE" | "TASK_FORK_SESSION_UNAVAILABLE" | "TASK_FORK_CYCLE" | "RUN_NOT_FOUND" | "NODE_NOT_FOUND" | "INVALID_EVENTS_OPTIONS" | "SANDBOX_BUNDLE_INVALID" | "SANDBOX_BUNDLE_TOO_LARGE" | "WORKFLOW_EXECUTION_FAILED" | "SANDBOX_EXECUTION_FAILED" | "TASK_HEARTBEAT_TIMEOUT" | "HEARTBEAT_PAYLOAD_TOO_LARGE" | "HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE" | "TASK_ABORTED" | "RUN_CANCELLED" | "RUN_NOT_RESUMABLE" | "RUN_OWNER_ALIVE" | "RUN_STILL_RUNNING" | "RUN_RESUME_CLAIM_LOST" | "RUN_RESUME_CLAIM_FAILED" | "RUN_RESUME_ACTIVATION_FAILED" | "AUTO_RESUME_GAVE_UP" | "RUN_HIJACKED" | "CONTINUATION_STATE_TOO_LARGE" | "INVALID_CONTINUATION_STATE" | "RALPH_MAX_REACHED" | "SCHEDULER_ERROR" | "SESSION_ERROR" | "TASK_ID_REQUIRED" | "TASK_MISSING_OUTPUT" | "TASK_EMPTY_PROMPT" | "WORKFLOW_RENDER_FAILED" | "DUPLICATE_ID" | "NESTED_LOOP" | "WORKTREE_EMPTY_PATH" | "MDX_PRELOAD_INACTIVE" | "CONTEXT_OUTSIDE_WORKFLOW" | "MISSING_OUTPUT" | "DEP_NOT_SATISFIED" | "BOUND_STALE" | "ASPECT_BUDGET_EXCEEDED" | "APPROVAL_OUTSIDE_TASK" | "APPROVAL_OPTIONS_REQUIRED" | "WORKFLOW_MISSING_DEFAULT" | "WORKFLOW_NOT_BUILT" | "TOOL_PATH_INVALID" | "TOOL_PATH_ESCAPE" | "TOOL_FILE_TOO_LARGE" | "TOOL_CONTENT_TOO_LARGE" | "TOOL_PATCH_TOO_LARGE" | "TOOL_PATCH_FAILED" | "TOOL_NETWORK_DISABLED" | "TOOL_GIT_REMOTE_DISABLED" | "TOOL_COMMAND_FAILED" | "TOOL_GREP_FAILED" | "AGENT_CLI_ERROR" | "AGENT_QUOTA_EXCEEDED" | "AGENT_CONFIG_INVALID" | "AGENT_RPC_FILE_ARGS" | "AGENT_BUILD_COMMAND" | "AGENT_DIAGNOSTIC_TIMEOUT" | "DB_MISSING_COLUMNS" | "DB_REQUIRES_BUN_SQLITE" | "DB_QUERY_FAILED" | "DB_WRITE_FAILED" | "PG_POOL_SATURATED" | "SMITHERS_BACKEND_CONFLICT" | "SMITHERS_MIGRATION_REQUIRED" | "STORAGE_ERROR" | "INTERNAL_ERROR" | "PROCESS_ABORTED" | "PROCESS_TIMEOUT" | "PROCESS_IDLE_TIMEOUT" | "PROCESS_SPAWN_FAILED" | "TASK_RUNTIME_UNAVAILABLE" | "SCHEMA_CHANGE_HOT" | "HOT_OVERLAY_FAILED" | "HOT_RELOAD_INVALID_MODULE" | "SCORER_FAILED" | "WORKFLOW_EXISTS" | "CLI_DB_NOT_FOUND" | "CLI_AGENT_UNSUPPORTED" | "TIME_TRAVEL_SIDE_EFFECT_BLOCKED" | "PI_HTTP_ERROR" | "EXTERNAL_BUILD_FAILED" | "SCHEMA_DISCOVERY_FAILED" | "OPENAPI_SPEC_LOAD_FAILED" | "OPENAPI_OPERATION_NOT_FOUND" | "OPENAPI_TOOL_EXECUTION_FAILED" | "ACCOUNT_INVALID" | "ACCOUNT_NOT_FOUND" | "ACCOUNT_DUPLICATE_LABEL" | "ACCOUNTS_FILE_INVALID" | "SINGLE_RUNNER_BUSY" | "SINGLE_RUNNER_CLOSED"; // ============================================================================= // Server // ============================================================================= type SmithersDb = import("@smithers-orchestrator/db/adapter").SmithersDb; type ServerOptions = { port?: number; db?: unknown; authToken?: string; maxBodyBytes?: number; rootDir?: string; allowNetwork?: boolean; headersTimeout?: number; requestTimeout?: number; }; type ServeOptions = { workflow: SmithersWorkflow; adapter: SmithersDb; runId: string; abort: AbortController; authToken?: string; metrics?: boolean; }; type GatewayTokenGrant = { role: string; scopes: string[]; userId?: string; tokenId?: string; issuedAtMs?: number; expiresAtMs?: number; revokedAtMs?: number; }; type GatewayAuthConfig = | { mode: "token"; tokens: Record; allowedOrigins?: string[]; // default [] (no Origin allowlist) } | { mode: "jwt"; issuer: string; audience: string | string[]; secret: string; scopesClaim?: string; // default "scope" roleClaim?: string; // default "role" userClaim?: string; // default "sub" defaultRole?: string; // default "operator" defaultScopes?: string[]; // default [] when scope claim is absent clockSkewSeconds?: number; // default 60; negative values clamp to 0 allowedOrigins?: string[]; // default [] (no Origin allowlist) } | { mode: "trusted-proxy"; trustedHeaders?: string[]; // default ["x-user-id","x-user-scopes","x-user-role"] allowedOrigins?: string[]; // default [] (no Origin allowlist) defaultRole?: string; // default "operator" defaultScopes?: string[]; // trusted-proxy: used when the scopes header is absent, else the request is rejected }; type GatewayDefaults = { cliAgentTools?: "all" | "explicit-only" }; type GatewayOperatorUiConfig = { path?: string; // default "/console" title?: string; props?: Record; }; type GatewayUiConfig = | true | { entry: string; path?: string; // gateway default "/"; workflow default "/workflows/" title?: string; props?: Record; }; type GatewayWebhookSignalConfig = { name: string; correlationIdPath?: string; runIdPath?: string; payloadPath?: string; }; type GatewayWebhookRunConfig = { enabled?: boolean; inputPath?: string; }; type GatewayWebhookConfig = { secret: string; signatureHeader?: string; signaturePrefix?: string; signal?: GatewayWebhookSignalConfig; run?: GatewayWebhookRunConfig; }; type GatewayRegisterOptions = { schedule?: string; webhook?: GatewayWebhookConfig; ui?: GatewayUiConfig; }; type GatewayOptions = { protocol?: number; features?: string[]; heartbeatMs?: number; auth?: GatewayAuthConfig; ui?: GatewayUiConfig; operatorUi?: GatewayOperatorUiConfig | false; defaults?: GatewayDefaults; maxBodyBytes?: number; maxPayload?: number; maxConnections?: number; eventWindowSize?: number; headersTimeout?: number; requestTimeout?: number; }; // ============================================================================= // Scorers (smithers-orchestrator/scorers) // ============================================================================= type ScoreResult = { score: number; reason?: string; meta?: Record }; type ScorerInput = { input: unknown; output: unknown; groundTruth?: unknown; context?: unknown; latencyMs?: number; outputSchema?: import("zod").ZodObject }; type ScorerFn = (input: ScorerInput) => Promise; type Scorer = { id: string; name: string; description: string; score: ScorerFn }; type SamplingConfig = | { type: "all" } | { type: "ratio"; rate: number } | { type: "none" }; type ScorerBinding = { scorer: Scorer; sampling?: SamplingConfig }; type ScorersMap = Record; type ScoreRow = { id: string; runId: string; nodeId: string; iteration: number; attempt: number; scorerId: string; scorerName: string; source: "live" | "batch"; score: number; reason: string | null; metaJson: string | null; inputJson: string | null; outputJson: string | null; groundTruthJson: string | null; contextJson: string | null; latencyMs: number | null; scoredAtMs: number; durationMs: number | null; }; type AggregateScore = { scorerId: string; scorerName: string; count: number; mean: number; min: number; max: number; p50: number; stddev: number; }; type AggregateOptions = { runId?: string; nodeId?: string; scorerId?: string; }; type ScorerContext = { runId: string; nodeId: string; iteration: number; attempt: number; input: unknown; output: unknown; latencyMs?: number; outputSchema?: import("zod").ZodObject; }; type LlmJudgeConfig = { id: string; name: string; description: string; judge: AgentLike; instructions: string; promptTemplate: (input: ScorerInput) => string; }; type CreateScorerConfig = { id: string; name: string; description: string; score: ScorerFn; }; // ============================================================================= // Memory (smithers-orchestrator/memory) // ============================================================================= type MemoryFact = { namespace: string; key: string; valueJson: string; schemaSig?: string | null; createdAtMs: number; updatedAtMs: number; ttlMs?: number | null }; type MemoryMessage = { id: string; threadId: string; role: string; contentJson: string; runId?: string | null; nodeId?: string | null; createdAtMs: number }; type MemoryThread = { threadId: string; namespace: string; title?: string | null; metadataJson?: string | null; createdAtMs: number; updatedAtMs: number }; type MemoryProvenance = { runId?: string | null; nodeId?: string | null; iteration?: number | null }; type MemoryNote = { id: string; namespace: string; body: string; kind?: string | null; tagsJson?: string | null; // JSON-encoded string array; null when the note has no tags author?: string | null; status: string; // free-form; conventionally pending | accepted | rejected statusChangedAtMs?: number | null; createdAtMs: number; runId?: string | null; nodeId?: string | null; iteration?: number | null; }; type SaveNoteInput = { namespace: MemoryNamespace; body: string; kind?: string; tags?: string[]; author?: string; status?: string; // defaults to "accepted" provenance?: MemoryProvenance; supersedes?: string[]; // note ids this note replaces id?: string; // provide to make retries idempotent }; type NoteReadFilter = { status?: string | string[] | "any"; includeSuperseded?: boolean; kind?: string; namespace?: MemoryNamespace; // scope searchNotes to one namespace of the kind }; type WorkingMemoryConfig< T extends import("zod").ZodObject = import("zod").ZodObject, > = { schema?: T; namespace: MemoryNamespace; ttlMs?: number; }; type SemanticRecallConfig = { topK?: number; namespace?: MemoryNamespace; similarityThreshold?: number; }; type MessageHistoryConfig = { lastMessages?: number; threadId?: string; }; type MemoryStore = { getFact(ns: MemoryNamespace, key: string): Promise; setFact(ns: MemoryNamespace, key: string, value: unknown, ttlMs?: number): Promise; deleteFact(ns: MemoryNamespace, key: string): Promise; listFacts(ns: MemoryNamespace): Promise; listAllFacts(): Promise; createThread(ns: MemoryNamespace, title?: string): Promise; getThread(threadId: string): Promise; deleteThread(threadId: string): Promise; saveMessage(msg: Omit & { createdAtMs?: number }): Promise; listMessages(threadId: string, limit?: number): Promise; countMessages(threadId: string): Promise; deleteExpiredFacts(): Promise; saveNote(input: SaveNoteInput): Promise; getNote(id: string): Promise; listNotes(ns: MemoryNamespace, filter?: NoteReadFilter): Promise; setNoteStatus(id: string, status: string): Promise; enableNoteSearch(kind: string): Promise; searchNotes(kind: string, query: string, limit?: number, filter?: NoteReadFilter): Promise; getFactEffect(ns: MemoryNamespace, key: string): Effect.Effect; setFactEffect(ns: MemoryNamespace, key: string, value: unknown, ttlMs?: number): Effect.Effect; deleteFactEffect(ns: MemoryNamespace, key: string): Effect.Effect; listFactsEffect(ns: MemoryNamespace): Effect.Effect; listAllFactsEffect(): Effect.Effect; createThreadEffect(ns: MemoryNamespace, title?: string): Effect.Effect; getThreadEffect(threadId: string): Effect.Effect; deleteThreadEffect(threadId: string): Effect.Effect; saveMessageEffect(msg: Omit & { createdAtMs?: number }): Effect.Effect; listMessagesEffect(threadId: string, limit?: number): Effect.Effect; countMessagesEffect(threadId: string): Effect.Effect; deleteExpiredFactsEffect(): Effect.Effect; saveNoteEffect(input: SaveNoteInput): Effect.Effect; getNoteEffect(id: string): Effect.Effect; listNotesEffect(ns: MemoryNamespace, filter?: NoteReadFilter): Effect.Effect; setNoteStatusEffect(id: string, status: string): Effect.Effect; enableNoteSearchEffect(kind: string): Effect.Effect; searchNotesEffect(kind: string, query: string, limit?: number, filter?: NoteReadFilter): Effect.Effect; }; type MemoryServiceApi = { readonly getFact: (ns: MemoryNamespace, key: string) => Effect.Effect; readonly setFact: (ns: MemoryNamespace, key: string, value: unknown, ttlMs?: number) => Effect.Effect; readonly deleteFact: (ns: MemoryNamespace, key: string) => Effect.Effect; readonly listFacts: (ns: MemoryNamespace) => Effect.Effect; readonly createThread: (ns: MemoryNamespace, title?: string) => Effect.Effect; readonly getThread: (threadId: string) => Effect.Effect; readonly deleteThread: (threadId: string) => Effect.Effect; readonly saveMessage: (msg: Omit & { createdAtMs?: number }) => Effect.Effect; readonly listMessages: (threadId: string, limit?: number) => Effect.Effect; readonly countMessages: (threadId: string) => Effect.Effect; readonly deleteExpiredFacts: () => Effect.Effect; readonly saveNote: (input: SaveNoteInput) => Effect.Effect; readonly getNote: (id: string) => Effect.Effect; readonly listNotes: (ns: MemoryNamespace, filter?: NoteReadFilter) => Effect.Effect; readonly setNoteStatus: (id: string, status: string) => Effect.Effect; readonly enableNoteSearch: (kind: string) => Effect.Effect; readonly searchNotes: (kind: string, query: string, limit?: number, filter?: NoteReadFilter) => Effect.Effect; readonly store: MemoryStore; }; type MemoryProcessorConfig = { processors?: string[]; }; type MemoryProcessor = { name: string; process: (store: MemoryStore) => Promise; processEffect: (store: MemoryStore) => Effect.Effect; }; type MemoryLayerConfig = { db: import("drizzle-orm/bun-sqlite").BunSQLiteDatabase>; }; // ============================================================================= // OpenAPI tools (smithers-orchestrator/openapi) // ============================================================================= type OpenApiAuth = | { type: "apiKey"; name: string; in: "header" | "query"; value: string } | { type: "bearer"; token: string } | { type: "basic"; username: string; password: string }; type OpenApiRefObject = { $ref: string; }; type OpenApiSchemaObject = { type?: string; format?: string; description?: string; properties?: Record; required?: string[]; items?: OpenApiSchemaObject | OpenApiRefObject; enum?: unknown[]; default?: unknown; nullable?: boolean; oneOf?: Array; anyOf?: Array; allOf?: Array; additionalProperties?: boolean | OpenApiSchemaObject | OpenApiRefObject; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; $ref?: string; }; type OpenApiParameterObject = { name: string; in: "query" | "header" | "path" | "cookie"; description?: string; required?: boolean; schema?: OpenApiSchemaObject | OpenApiRefObject; deprecated?: boolean; }; type OpenApiRequestBodyObject = { description?: string; required?: boolean; content: Record; }; type OpenApiOperationObject = { operationId?: string; summary?: string; description?: string; parameters?: Array; requestBody?: OpenApiRequestBodyObject | OpenApiRefObject; responses?: Record; tags?: string[]; deprecated?: boolean; }; type OpenApiPathItem = { get?: OpenApiOperationObject; post?: OpenApiOperationObject; put?: OpenApiOperationObject; delete?: OpenApiOperationObject; patch?: OpenApiOperationObject; parameters?: Array; }; type OpenApiSpec = { openapi: string; info: { title: string; version: string; description?: string; }; servers?: Array<{ url: string; description?: string; variables?: Record; }>; paths: Record; components?: { schemas?: Record; parameters?: Record; requestBodies?: Record; }; }; type OpenApiToolsOptions = { baseUrl?: string; headers?: Record; auth?: OpenApiAuth; include?: string[]; exclude?: string[]; namePrefix?: string; operations?: Record< string, | false | { include?: boolean; name?: string; description?: string; responseExamples?: Array<{ status?: string | number; description?: string; value: unknown; }>; } >; }; // ============================================================================= // CreateSmithers // ============================================================================= type CreateSmithersOptions = { readableName?: string; description?: string; alertPolicy?: SmithersAlertPolicy; dbPath?: string; journalMode?: string; /** Maximum connections in the shared PostgreSQL pool for one normalized URL; defaults to 16. */ postgresPoolMax?: number; }; type CreateSmithersPostgresOptions = CreateSmithersOptions & ( | { provider?: "postgres"; connectionString?: string; connection?: object } | { provider: "pglite"; dataDir?: string } ); // Named export from "smithers-orchestrator". The returned `smithers` property is // the workflow wrapper; there is no default-exported top-level smithers function. declare function createSmithers>>( schemas: Schemas, opts?: CreateSmithersOptions, ): CreateSmithersApi; declare function createSmithersPostgres>>( schemas: Schemas, opts?: CreateSmithersPostgresOptions, ): Promise & { close: () => Promise }>; type SchemaOutput = Extract< Schema[keyof Schema], import("zod").ZodObject >; type RuntimeSchema = Schema extends { input: infer Input } ? Omit & { input: Input extends import("zod").ZodTypeAny ? import("zod").infer : Input } : Schema; type CreateSmithersApi = { Workflow: (props: WorkflowProps) => React.ReactElement; Approval: (props: ApprovalProps>) => React.ReactElement; Task: (props: TaskProps, D>) => React.ReactElement; Sequence: typeof Sequence; Parallel: typeof Parallel; MergeQueue: typeof MergeQueue; Branch: typeof Branch; Loop: typeof Loop; Ralph: typeof Ralph; ContinueAsNew: typeof ContinueAsNew; continueAsNew: typeof continueAsNew; Worktree: typeof Worktree; Sandbox: (props: SandboxProps) => React.ReactElement; Signal: >(props: SignalProps) => React.ReactElement; Timer: typeof Timer; useCtx: () => SmithersCtx>; smithers: ( build: (ctx: SmithersCtx>) => React.ReactElement, opts?: SmithersWorkflowOptions, ) => SmithersWorkflow>; db: import("drizzle-orm/bun-sqlite").BunSQLiteDatabase>; tables: { [K in keyof Schema]: unknown }; outputs: { [K in keyof Schema]: Schema[K] }; }; type SerializedCtx = { runId: string; iteration: number; iterations: Record; input: unknown; outputs: OutputSnapshot; }; type HostNodeJson = | { kind: "element"; tag: string; props: Record; rawProps: Record; children: HostNodeJson[]; } | { kind: "text"; text: string; }; type ExternalSmithersConfig>> = { schemas: S; agents: Record; buildFn: (ctx: SerializedCtx) => HostNodeJson; dbPath?: string; }; declare function createExternalSmithers>>( config: ExternalSmithersConfig, ): SmithersWorkflow & { tables: Record; cleanup: () => void }; // ============================================================================= // Observability (smithers-orchestrator/observability) // ============================================================================= type SmithersLogFormat = "json" | "pretty"; type SmithersObservabilityService = { emit(event: SmithersEvent): void | Promise }; type SmithersObservabilityOptions = { service?: SmithersObservabilityService; logFormat?: SmithersLogFormat }; type ResolvedSmithersObservabilityOptions = SmithersObservabilityOptions & { metricsPort?: number; metricsPath?: string }; ``` ## Error reporting `RunOptions.onError` receives a normalized `SmithersError`, the original error value, and the available run, node, iteration, and attempt context. It fires once per `NodeFailed` or `RunFailed` event: a retrying task produces one node report per failed attempt, and a terminal run failure adds a run report. Smithers catches reporter failures so they can't change the run outcome. ```ts runWorkflow(workflow, { input: {}, onError: (r) => Sentry.captureException(r.error, { extra: { runId: r.runId, nodeId: r.nodeId, phase: r.phase } }), }); ``` For canonical, machine-checked types, install `smithers-orchestrator` and use editor go-to-definition. For runtime errors, see Errors. --- ## Error Reference > Exhaustive Smithers error codes, typed error helpers, and HTTP API error responses. `SmithersErrorInstance` is a typed, code-bearing `Error` subclass used throughout Smithers internals. It surfaces when `runWorkflow` throws, in `NodeFailed` events emitted during execution, and as JSON in HTTP API error responses. The imports below are the full error utility surface. **`build agent command: undefined is not an object (evaluating 'schema._zod.def')`**: your task `output` schema is a **Zod v3** object; Smithers reads schema metadata via Zod v4 internals (`_zod.def`). Install Zod v4 (`bun add zod@^4`) and import `z` from it. Deterministic configuration error: re-running won't fix it. ```ts import { ERROR_REFERENCE_URL, SmithersErrorInstance, errorToJson, getSmithersErrorDefinition, getSmithersErrorDocsUrl, isKnownSmithersErrorCode, isSmithersError, knownSmithersErrorCodes, } from "smithers-orchestrator"; import type { KnownSmithersErrorCode, SmithersError, SmithersErrorCode, } from "smithers-orchestrator"; ``` Every built-in `SmithersErrorInstance` carries three pieces of documentation metadata: | Field | Meaning | |---|---| | `message` | Human-readable description followed by a docs URL, e.g. `"Input failed validation. See https://…"` | | `summary` | Raw message without the docs suffix. | | `docsUrl` | Reference URL for Smithers errors. | Use `KnownSmithersErrorCode` for an exhaustive switch over built-in Smithers codes. `SmithersErrorCode` includes the `(string & {})` escape hatch for user-defined custom codes. | Export | Kind | Description | |---|---|---| | `SmithersErrorInstance` | class | Runtime error class used throughout Smithers internals. | | `isSmithersError(err)` | function | Type guard for values carrying a Smithers-style `code`. | | `isKnownSmithersErrorCode(code)` | function | Narrows a string to the built-in exhaustive error-code union. | | `knownSmithersErrorCodes` | value | Array of every built-in Smithers error code on this page. | | `getSmithersErrorDocsUrl(code)` | function | Returns the docs URL appended to built-in error messages. | | `getSmithersErrorDefinition(code)` | function | Returns category, description, and details metadata for known codes. | | `errorToJson(err)` | function | Serializes `name`, `message`, `summary`, `docsUrl`, `code`, `details`, `cause`, and `stack`. | | `ERROR_REFERENCE_URL` | value | Base docs URL for Smithers runtime errors. | | `KnownSmithersErrorCode` | type | Exact built-in Smithers code union. | | `SmithersErrorCode` | type | Built-in codes plus the custom string escape hatch. | | `SmithersError` | type | Public typed shape for serialized Smithers errors. | ```ts import { Effect } from "effect"; import { runWorkflow } from "smithers-orchestrator"; try { await Effect.runPromise(runWorkflow(workflow, { input: {} })); } catch (err) { if (isSmithersError(err) && isKnownSmithersErrorCode(err.code)) { switch (err.code) { case "INVALID_INPUT": console.error("Bad input:", err.summary); break; case "AGENT_CLI_ERROR": console.error("Agent failed:", err.summary); break; default: console.error(`[${err.code}] ${err.summary}`); } console.error("Docs:", err.docsUrl); } } ``` ## Engine | Code | When | Details | |---|---|---| | `INVALID_INPUT` | Workflow input fails validation or the runtime receives a non-object input payload. | -- | | `MISSING_INPUT` | A resume run references an input row missing from the database. | -- | | `MISSING_INPUT_TABLE` | Workflow schema doesn't expose the expected input table during resume or hydration. | -- | | `RESUME_METADATA_MISMATCH` | Stored run metadata no longer matches the workflow being resumed. Editing the workflow file or an imported module between stop and resume triggers this (resume hashes file content, not git; no commit required). Fork/replay onto the edit or start fresh; revert the file to resume the original run. | `mismatches`, `existing`, `current` | | `UNKNOWN_OUTPUT_SCHEMA` | A task references an output table not present in the schema registry. | -- | | `INVALID_OUTPUT` | Agent output cannot be parsed or validated against the declared output schema. | -- | | `WORKTREE_CREATE_FAILED` | Smithers fails to create or hydrate a git or jj worktree for a task. | `{ worktreePath, vcsType, branch? }` | | `VCS_NOT_FOUND` | No supported git or jj repository root is found for the workflow. | `{ rootDir }` | | `SNAPSHOT_NOT_FOUND` | A requested time-travel snapshot or frame does not exist. | `{ runId, frameNo }` | | `TIME_TRAVEL_SIDE_EFFECT_BLOCKED` | A time-travel operation would cross an external side effect that was not reverted or explicitly forced. | `{ runId, operation, report }` | | `VCS_WORKSPACE_CREATE_FAILED` | Smithers fails to materialize a jj workspace for time-travel or replay. | `{ runId, frameNo, vcsPointer, workspacePath }` | | `TASK_EMPTY_PROMPT` | A `` prompt renders to an empty string, invoking the agent with no input. | `{ nodeId, iteration }` | | `WORKFLOW_RENDER_FAILED` | The workflow component throws while rendering the graph (e.g. a hook called outside a component render, or a bug in the workflow function). | `{ workflowPath }` | | `TASK_TIMEOUT` | A task compute callback exceeds its configured timeout. | `{ nodeId, attempt, timeoutMs }` | | `TASK_HIJACK_UNSUPPORTED` | A task requests auto-hijack but its agent cannot provide a resumable session or conversation. | `{ nodeId, agentId? }` | | `TASK_FORK_SOURCE_NOT_COMPLETE` | A forked task began executing but its fork source hasn't completed, so no session snapshot exists yet. | `{ nodeId, forkSource }` | | `TASK_FORK_SESSION_UNAVAILABLE` | A `` cannot obtain a usable agent session snapshot: either the forking task isn't an agent task, or the source completed without producing a forkable conversation (e.g. a compute/static, skipped, or cancelled source). | `{ nodeId, forkSource }` | | `TASK_ABORTED` | A running task is aborted through an AbortSignal or shutdown path. | -- | | `RUN_NOT_FOUND` | A CLI or engine command references a run ID missing from the database. | `{ runId }` | | `NODE_NOT_FOUND` | A CLI command references a node ID missing for the given run. | `{ runId, nodeId }` | | `SANDBOX_BUNDLE_INVALID` | A sandbox bundle fails validation (missing README, invalid manifest, etc.). | `{ bundlePath }` | | `SANDBOX_BUNDLE_TOO_LARGE` | A sandbox bundle exceeds the maximum allowed size. | `{ bundlePath, maxBytes }` | | `WORKFLOW_EXECUTION_FAILED` | A child or builder workflow exits unsuccessfully without surfacing a typed error payload. | `{ status }` | | `SANDBOX_EXECUTION_FAILED` | Sandbox setup or execution fails before a more specific sandbox error can be emitted. | `{ sandboxId, runId?, maxConcurrent?, activeSandboxCount? }` | | `TASK_HEARTBEAT_TIMEOUT` | A task heartbeat timeout expires while the task is still running. | `{ nodeId, iteration, attempt, timeoutMs, staleForMs, lastHeartbeatAtMs }` | | `HEARTBEAT_PAYLOAD_TOO_LARGE` | A task heartbeat payload exceeds the maximum persisted checkpoint size. | `{ dataSizeBytes, maxBytes }` | | `HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE` | A task heartbeat payload contains values not serializable to JSON. | `{ path, valueType? }` | | `RUN_CANCELLED` | A run is cancelled while runtime work is still active. | `{ runId }` | | `RUN_NOT_RESUMABLE` | A resume request targets a run state that cannot be resumed. | `{ runId, status }` | | `RUN_OWNER_ALIVE` | A resume attempt is skipped because the process that started the run is still alive (heartbeating): normal behavior that prevents two processes from running the same workflow simultaneously. | `{ runId, runtimeOwnerId }` | | `RUN_STILL_RUNNING` | A recovery or resume operation finds a run still active. | `{ runId }` | | `RUN_RESUME_CLAIM_LOST` | A runtime loses the resume claim before it can update the run. | `{ runId, runtimeOwnerId }` | | `RUN_RESUME_CLAIM_FAILED` | A runtime cannot claim a stale run for resume. | `{ runId, runtimeOwnerId }` | | `RUN_RESUME_ACTIVATION_FAILED` | A claimed run cannot be moved back into active execution. | `{ runId, runtimeOwnerId }` | | `AUTO_RESUME_GAVE_UP` | The supervisor stops auto-resuming a run after consecutive detached resumes died before the engine activated, and marks the run failed with the resume log location. The run stays manually resumable once the startup failure is fixed. | `{ attempts, lastClaimOwnerId, logFile, logTail? }` | | `RUN_HIJACKED` | A run is interrupted because another runtime hijacked execution. | `{ runId, hijackTarget }` | | `CONTINUATION_STATE_TOO_LARGE` | Continue-as-new state exceeds the configured serialized size limit. | `{ runId, sizeBytes, maxBytes }` | | `INVALID_CONTINUATION_STATE` | Continue-as-new state cannot be parsed or applied. | -- | | `RALPH_MAX_REACHED` | A Ralph loop reaches maxIterations with fail-on-max behavior. | `{ ralphId, maxIterations }` | | `SCHEDULER_ERROR` | The scheduler cannot produce a valid execution decision. | -- | | `SESSION_ERROR` | The workflow session state machine reaches an invalid or failed state. | -- | ## Components | Code | When | Details | |---|---|---| | `TASK_ID_REQUIRED` | `` is missing a valid string id. | -- | | `TASK_MISSING_OUTPUT` | `` is missing its output prop. | `{ nodeId }` | | `TASK_FORK_SOURCE_NOT_FOUND` | A `` references a source task id not present in the workflow graph, including one that exists only in an unselected branch. | `{ nodeId, forkSource }` | | `TASK_FORK_CYCLE` | A `` introduces a dependency cycle, directly or indirectly. | `{ nodeId, forkSource }` | | `DUPLICATE_ID` | Two nodes with the same runtime id are mounted in one workflow graph. | `{ kind, id }` | | `NESTED_LOOP` | `` or `` is nested inside another loop construct Smithers doesn't support. | -- | | `WORKTREE_EMPTY_PATH` | `` is mounted with an empty path. | -- | | `MDX_PRELOAD_INACTIVE` | A prompt object is rendered without the MDX preload layer being active. | -- | | `CONTEXT_OUTSIDE_WORKFLOW` | Workflow context access happens outside an active Smithers workflow render. | -- | | `MISSING_OUTPUT` | Code calls `ctx.output()` for a missing node result. | `{ nodeId, iteration }` | | `DEP_NOT_SATISFIED` | A typed dep on `` references an upstream output not yet produced. | `{ taskId, depKey, resolvedNodeId }` | | `BOUND_STALE` | A `` authority row no longer matches the digest captured by `ctx.prove()`; the task parks while the run remains resumable. | `{ nodeId, bindings }` | | `ASPECT_BUDGET_EXCEEDED` | An Aspects budget (tokens or latency) has been exceeded. | `{ kind, limit, current }` | | `APPROVAL_OUTSIDE_TASK` | `` is resolved outside the active task runtime. | -- | | `APPROVAL_OPTIONS_REQUIRED` | An approval mode requiring explicit options is missing them. | -- | | `WORKFLOW_MISSING_DEFAULT` | A workflow module does not export a default Smithers workflow. | -- | | `WORKFLOW_NOT_BUILT` | A workflow's default export is a raw component or JSX element instead of the object returned by `smithers(...)`. | -- | ## Tools | Code | When | Details | |---|---|---| | `TOOL_PATH_INVALID` | A filesystem tool receives a non-string path. | -- | | `TOOL_PATH_ESCAPE` | A filesystem tool resolves a path outside the sandbox root, including through symlinks. | -- | | `TOOL_FILE_TOO_LARGE` | A read or edit operation exceeds the configured file size limit. | -- | | `TOOL_CONTENT_TOO_LARGE` | A write operation exceeds the configured content size limit. | -- | | `TOOL_PATCH_TOO_LARGE` | An edit patch exceeds the configured patch size limit. | -- | | `TOOL_PATCH_FAILED` | A unified diff patch cannot be applied to the target file. | -- | | `TOOL_NETWORK_DISABLED` | The bash tool tries to reach a non-loopback endpoint while network access is disabled. Loopback (localhost, `127.0.0.1`, `*.localhost`, unix sockets) is always allowed. | Pass `--allow-network` (CLI) or `allowNetwork: true` (run options) when the command genuinely needs egress. | | `TOOL_GIT_REMOTE_DISABLED` | The bash tool attempts a remote git operation while network access is disabled. | Pass `--allow-network` (CLI) or `allowNetwork: true` (run options), or perform the remote operation outside the sandboxed tool. | | `TOOL_COMMAND_FAILED` | A bash tool command exits with a non-zero status. | -- | | `TOOL_GREP_FAILED` | The grep tool fails with an rg execution error. | -- | ## Agents | Code | When | Details | |---|---|---| | `AGENT_CLI_ERROR` | A CLI-backed agent exits unsuccessfully, streams an explicit error, or its RPC transport fails. | -- | | `AGENT_QUOTA_EXCEEDED` | An agent provider returns a usage-limit or quota error: transient, never consuming the retry budget. The task fails over to the next agent in its `agent={[...]}` chain, and the run only pauses (`waiting-quota`) once every agent in the chain is rate-limited, then until the earliest reset time among them. | `{ agentId?, agentEngine?, agentModel?, quotaResetAtMs?, resetHint? }` | | `AGENT_CONFIG_INVALID` | A CLI-backed agent fails with a non-retryable configuration error such as an unknown model, missing LLM, or unsupported model. | -- | | `AGENT_RPC_FILE_ARGS` | Pi RPC mode is used with file arguments the transport doesn't support. | -- | | `AGENT_BUILD_COMMAND` | An agent implementation forbids `buildCommand()` because it uses a custom `generate()` transport. | -- | | `AGENT_DIAGNOSTIC_TIMEOUT` | An internal agent diagnostic check exceeds the per-check timeout budget. | -- | ## Database | Code | When | Details | |---|---|---| | `DB_MISSING_COLUMNS` | A table used by Smithers doesn't expose required columns such as `runId` or `nodeId`. | -- | | `DB_REQUIRES_BUN_SQLITE` | The database adapter is not backed by a Bun SQLite client with `exec()`. | -- | | `DB_QUERY_FAILED` | A database read query throws or rejects while running inside an Effect. | -- | | `DB_WRITE_FAILED` | A database write or migration fails, including after SQLite retry exhaustion. | -- | | `PG_POOL_SATURATED` | Every connection in the shared PostgreSQL pool stayed busy for a full acquire wait, so the bound is too low for the concurrent workflows or a query is leaking a client. | `{ identity, max, maxSource, acquireTimeoutMs, totalCount, idleCount, waitingCount, configKnob }` | | `SMITHERS_BACKEND_CONFLICT` | Multiple Smithers backend stores contain run history and no `migrated.json` receipt explains the divergence. | `{ populatedBackends, stores }` | | `SMITHERS_MIGRATION_REQUIRED` | A physical Smithers store has run data but the resolved backend points elsewhere, so the history stays invisible until you migrate or pin the existing backend. | `{ sourceBackend, targetBackend, dbPath?, location?, runCount, schemaVersion, resolvedBackend? }` | | `STORAGE_ERROR` | A storage service operation fails before surfacing a more specific database code. | -- | ### Migration errors `smithers migrate` preserves the source SQLite store by default: if the legacy `smithers.db` can't be copied into the target backend, Smithers leaves the original file untouched and reports the first actionable failure. Corrupt, malformed, encrypted, or non-SQLite source files surface as `DB_QUERY_FAILED` with the source `dbPath` in `details`; the message tells you to verify the file with `sqlite3 'PRAGMA integrity_check'` and restore from backup or start fresh if SQLite confirms corruption. Source files that exist but can't be opened also surface as `DB_QUERY_FAILED`, pointing at common operational causes: another process holding the file, unreadable permissions, or a copied SQLite file missing its `smithers.db-wal` / `smithers.db-shm` sidecars. For Postgres migrations, `smithers migrate --to postgres` validates the target connection string before opening the source store: a missing `--url`, `SMITHERS_POSTGRES_URL`, or `DATABASE_URL` fails fast with `INVALID_INPUT`, so connection setup problems aren't hidden behind unrelated source-store errors. ## Effect / Runtime | Code | When | Details | |---|---|---| | `INTERNAL_ERROR` | An unexpected internal exception crossed an Effect boundary without a more specific Smithers code. | -- | | `PROCESS_ABORTED` | A spawned child process is aborted by signal or shutdown. | `{ command, args, cwd }` | | `PROCESS_TIMEOUT` | A spawned child process exceeds its total timeout. | `{ command, args, cwd, timeoutMs }` | | `PROCESS_IDLE_TIMEOUT` | A spawned child process stops producing output longer than its idle timeout. | `{ command, args, cwd, idleTimeoutMs }` | | `PROCESS_SPAWN_FAILED` | The runtime cannot spawn the requested child process. | `{ command, args, cwd }` | | `TASK_RUNTIME_UNAVAILABLE` | Builder task runtime APIs are accessed outside an executing step. | -- | | `SINGLE_RUNNER_BUSY` | `closeSingleRunnerRuntime()` was called while a run or a task dispatch still holds the process-local SingleRunner runtime. The runtime is left open and usable; await the outstanding runs and close again. | `{ state, runIds, executionIds }` | | `SINGLE_RUNNER_CLOSED` | A run or task dispatch tried to start after `closeSingleRunnerRuntime()` began. Call `reopenSingleRunnerRuntime()` to allow the runtime to be rebuilt lazily. | `{ state, operation }` | ## Hot Reload | Code | When | Details | |---|---|---| | `SCHEMA_CHANGE_HOT` | Hot reload detects a schema change requiring a full restart. | -- | | `HOT_OVERLAY_FAILED` | Building or cleaning the generated hot-reload overlay fails. | -- | | `HOT_RELOAD_INVALID_MODULE` | A hot-reloaded workflow module doesn't export a valid default workflow build. | -- | ## Scorers | Code | When | Details | |---|---|---| | `SCORER_FAILED` | A scorer throws or rejects while Smithers is evaluating a result. | -- | ## CLI | Code | When | Details | |---|---|---| | `INVALID_EVENTS_OPTIONS` | The smithers events command receives invalid filter options. | -- | | `WORKFLOW_EXISTS` | The workflow creation CLI refuses to overwrite an existing workflow file. | -- | | `CLI_DB_NOT_FOUND` | A CLI command cannot find a nearby `smithers.db` file. | -- | | `CLI_AGENT_UNSUPPORTED` | The ask command selects an agent integration Smithers doesn't support in that mode. | -- | ## Integrations | Code | When | Details | |---|---|---| | `PI_HTTP_ERROR` | The Pi or server integration receives a non-success HTTP response from Smithers. | -- | | `EXTERNAL_BUILD_FAILED` | An external workflow host fails to build a Smithers HostNode payload. | `{ scriptPath, error?, exitCode?, stderr?, stdout? }` | | `SCHEMA_DISCOVERY_FAILED` | External workflow schema discovery fails or returns invalid output. | `{ scriptPath, error?, exitCode?, stderr? }` | | `OPENAPI_SPEC_LOAD_FAILED` | An OpenAPI spec cannot be loaded or parsed. | -- | | `OPENAPI_OPERATION_NOT_FOUND` | The requested operationId doesn't exist in the OpenAPI spec. | -- | | `OPENAPI_TOOL_EXECUTION_FAILED` | An OpenAPI tool call fails during HTTP execution. | -- | | `ACCOUNT_INVALID` | An account entry, label, provider, or provider-specific configuration is invalid. | -- | | `ACCOUNT_NOT_FOUND` | An account operation references an unregistered label. | -- | | `ACCOUNT_DUPLICATE_LABEL` | An account add operation would create a duplicate label without replace enabled. | -- | | `ACCOUNTS_FILE_INVALID` | The accounts.json file isn't valid JSON, or doesn't match the expected account registry schema after tolerant entry filtering. | -- | Unknown legacy account providers are tolerated entry-by-entry. For example, an old `"provider": "gemini"` subscription is skipped with a warning naming the account label and valid providers, while the remaining valid accounts still load. Such an entry is left out of the active account list but preserved verbatim in `accounts.json` across later `agents add` and `agents remove` calls, so an unrelated change never destroys the credentials its `configDir` points at. Run `bunx smithers-orchestrator agents remove