Smithers Orchestrator (smithers-orchestrator)
JSX workflow engine for Claude Code: render → execute active nodes → persist to SQLite → rerender until stable/stop. Durable by default when you store control-flow in db.state (SQLite), not React useState. maxIterations is mandatory for safety.
Use when
- •Multi-step repo automation (PR finalize, CI recovery, release smoke tests, refactors)
- •Needs resume after interruption (power/network/tool failures)
- •Needs observability (phases/steps/tools/agents logged + NDJSON streams)
- •Needs constrained tool permissions per agent step
Fast requirements (ask once, then write code)
- •Goal + acceptance checks (tests pass? lint clean? diff reviewed?)
- •Allowed tools per step (Read/Write/Edit/Bash/…)
- •Stop limits:
maxIterations, optional timeout + stop conditions - •Branching needs: retries, human checkpoint(s), optional phases (
skipIf)
Setup
Claude Code plugin install (optional; for end-users running Smithers inside Claude Code)
/plugin marketplace add evmts/smithers /plugin install smithers@smithers
Project install + TSX config (Bun)
bun add smithers-orchestrator
Option A: tsconfig.json (recommended)
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "smithers-orchestrator"
}
}
Option B: Per-file pragma
If not using tsconfig, add this pragma at the top of each .tsx file:
/** @jsxImportSource smithers-orchestrator */
bunfig.toml (optional, for dev mode)
[dev] jsx = "react"
Canonical workflow template (resumable, phases+steps)
Create workflows/smithers-example.tsx:
#!/usr/bin/env bun
import {
createSmithersDB,
createSmithersRoot,
SmithersProvider,
Phase,
Step,
Claude,
} from "smithers-orchestrator";
const db = createSmithersDB({ path: ".smithers/smithers-example" });
// Resume if an execution is incomplete; otherwise start a new one.
const existing = db.execution.findIncomplete();
const executionId =
existing?.id ??
db.execution.start({
name: "Smithers Example",
script: "workflows/smithers-example.tsx",
});
function Workflow() {
return (
<SmithersProvider
db={db}
executionId={executionId}
maxIterations={25}
globalTimeout={30 * 60 * 1000}
>
<Phase name="Plan">
<Step name="scan">
<Claude model="sonnet" allowedTools={["Read", "Glob", "Grep"]}>
Inspect the repo and produce a short plan:
- key files
- risks
- commands to run
</Claude>
</Step>
</Phase>
<Phase name="Implement">
<Step name="changes">
<Claude model="sonnet" allowedTools={["Read", "Edit", "Write"]}>
Implement the planned changes.
Keep edits minimal and consistent with project style.
</Claude>
</Step>
</Phase>
<Phase name="Verify">
<Step name="tests">
<Claude model="sonnet" allowedTools={["Bash", "Read"]}>
Run the project's tests and fix failures until green.
</Claude>
</Step>
</Phase>
<Phase name="Report">
<Step name="summary">
<Claude model="haiku" allowedTools={["Read"]}>
Summarize what changed and how it was verified.
</Claude>
</Step>
</Phase>
</SmithersProvider>
);
}
const root = createSmithersRoot({ db, executionId });
await root.mount(Workflow);
db.close();
Run:
bun workflows/smithers-example.tsx
Notes:
- •DB APIs are synchronous; close DB at end.
- •Resume relies on
db.execution.findIncomplete().
Execution model (what to rely on)
Durable iteration ("Ralphing") rules
- •Workflow progresses via rerenders; each rerender is an "iteration".
- •Always set
maxIterations(default is 100 if you omit it). - •For durable branching/retry: store state in
db.state(SQLite); read with reactive queries (useQueryValue, etc.) rather than ReactuseState.
Phases + Steps (sequential semantics)
- •A
<Phase>auto-advances when all child<Step>s complete. - •Phase requires ≥1 Step to auto-advance; otherwise it will not progress without manual logic (
onComplete+ your own state). - •Only the active phase renders children under the registry context (provided by
SmithersProvider).
Parallel (experimental)
- •
<Parallel>runs children concurrently inside a Step; semantics are experimental and have known limitations.
Task (presentational)
- •
<Task>is just a checkbox-like rendering element; it is notdb.tasks.*.
SQLite state (db.state): minimal contract
State values are JSON-serialized on write and parsed on read (get, getAll). History stores old/new as JSON strings; add optional trigger strings for causality/audit.
| Method | Use |
|---|---|
db.state.get<T>(key) | read (parsed) |
db.state.set(key, value, trigger?) | write + optional cause |
db.state.setMany(obj, trigger?) | atomic multi-write |
db.state.getAll() | full state object |
db.state.history(key?, limit?) | transitions (old/new JSON strings) |
db.state.has(key) / db.state.delete(key, trigger?) / db.state.reset() | utilities |
Logging & monitoring (where to look)
Default log layout: .smithers/logs/ plus execution-scoped logs under .smithers/executions/<execution-id>/logs/ including stream.ndjson + stream.summary.json.
NDJSON event types:
- •
text-end,reasoning-end,tool-call,tool-result,error
Optional summarization (large outputs) via Haiku:
- •
ANTHROPIC_API_KEYrequired - •Thresholds:
SMITHERS_SUMMARY_THRESHOLD(default 50 lines),SMITHERS_SUMMARY_CHAR_THRESHOLD(default 4000),SMITHERS_SUMMARY_MAX_CHARS(default 20000) - •Model:
SMITHERS_SUMMARY_MODEL(defaultclaude-3-haiku-20240307)
Anti-Patterns (CRITICAL)
Stop Conditions Are An Anti-Pattern
Hill climbing never stops - it either fails to climb or hits diminishing returns. Don't ask "are we done?"
// ❌ BAD: Explicit "done" check
<Ralph condition={() => !isDone()}>...</Ralph>
// ✅ BETTER: Stop when no improvement in last N runs
<Ralph condition={() => recentImprovements.slice(-5).some(i => i > 0)}>
Over-Engineering Phases Reduces LLM Flexibility
Agents are capable. Give them large tasks with related context. Don't micromanage into tiny steps.
// ❌ BAD: Over-engineered into many phases
<Phase><Claude>Analyze</Claude></Phase>
<Phase><Claude>Plan</Claude></Phase>
<Phase><Claude>Implement step 1</Claude></Phase>
<Phase><Claude>Implement step 2</Claude></Phase>
<Phase><Claude>Test</Claude></Phase>
<Phase><Claude>Review</Claude></Phase>
// ✅ BETTER: Single comprehensive prompt
<Claude>{`Implement feature X with tests.
Analyze patterns, follow conventions, ensure 80% coverage.
Report: files changed, tests added, coverage delta.`}</Claude>
// ✅ OR: Ralph for iterative improvement
<Ralph condition={coverageBelow80} maxIterations={10}>
<Claude>{`Improve coverage to 80%. Report what you improved.`}</Claude>
</Ralph>
When Phases DO Make Sense
Only when context completely shifts (different language, different codebase, no shared context):
// ✅ GOOD: Distinct context shifts <Phase name="backend"><Claude>Implement Go API</Claude></Phase> <Phase name="frontend"><Claude>Implement React UI</Claude></Phase>
Rule of Thumb
If you're writing more than 3 phases, you're over-engineering. Give the agent a comprehensive prompt and let it ralph.
Agent Components
Claude
Claude CLI agent for AI tasks.
<Claude
model="sonnet" // "sonnet" | "opus" | "haiku"
allowedTools={["Read", "Bash"]}
maxTurns={10}
permissionMode="bypassPermissions"
>
Your prompt here
</Claude>
Amp
Amp CLI agent (alternative to Claude CLI). Uses amp CLI under the hood.
<Amp
mode="smart" // "smart" (SOTA) | "rush" (faster/cheaper)
maxTurns={50} // handled via prompt, not CLI flag
permissionMode="bypassPermissions"
systemPrompt="Be concise"
labels={["feature", "urgent"]}
>
Your prompt here
</Amp>
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The prompt text |
mode | 'smart' | 'rush' | 'smart' | Agent mode |
maxTurns | number | - | Turn limit (prompt-injected) |
systemPrompt | string | - | System prompt |
permissionMode | 'default' | 'acceptEdits' | 'bypassPermissions' | 'default' | Permission handling |
timeout | number | - | Timeout in ms |
cwd | string | - | Working directory |
continueThread | boolean | - | Continue from last thread |
resumeThread | string | - | Resume specific thread ID |
labels | string[] | - | Thread labels |
Guardrails for agents generating Smithers code
- •Prefer single
<Claude>with comprehensive prompt over many phases for related tasks. - •Use
<Ralph>for iterative improvement with improvement-based stop conditions (not "done" checks). - •Use phases only when context completely shifts (different language/codebase).
- •Use
db.statefor any branching/retry/human checkpoints. - •Scope tool permissions per
<Claude>or<Amp>call and choose conservativepermissionModeby default. - •Never omit
maxIterations.
Pointer files
This plugin is intentionally "single manual": full patterns live in EXAMPLES.md (end-to-end runnable scripts).