Worktree Feature: Full Pipeline
The most complex Smithers example: multiple CLI agents (Claude Code, OpenAI Codex) through a full development lifecycle.
Pipeline
- Discover: Read PRD, break into ordered independent tickets
- Implement: Write code end-to-end per ticket
- Validate: Run
bun test
- Review: Claude + Codex review in parallel
- ReviewFix: Address review issues
- Report: Generate final report
Steps 2—5 loop via <Loop> until both reviewers approve or max iterations reached.
Schema Setup: smithers.ts
Schema definitions are in smithers.ts. See Worktree Feature Schemas for the full field list. The file ends with the createSmithers(...) registration call:
// scripts/worktree-feature/smithers.ts
export const { Workflow, Task, useCtx, smithers, tables, outputs } = createSmithers({
discover: DiscoverOutput,
implement: ImplementOutput,
validate: ValidateOutput,
review: ReviewOutput,
reviewFix: ReviewFixOutput,
report: ReportOutput,
}, {
dbPath: `${process.env.HOME}/.cache/smithers/worktree-feature.db`,
journalMode: "DELETE",
});
Entry Point: workflow.tsx
// scripts/worktree-feature/workflow.tsx
import { Sequence, Branch } from "smithers-orchestrator";
import { Discover, TicketPipeline } from "./components";
import { Workflow, smithers, outputs } from "./smithers";
export default smithers((ctx) => {
const discoverOutput = ctx.latest("discover", "discover-codex");
const tickets = discoverOutput?.tickets ?? [];
const unfinishedTickets = tickets.filter(
(t: any) => !ctx.latest("report", `${t.id}:report`)
);
return (
<Workflow name="worktree-feature">
<Sequence>
<Branch if={tickets.length === 0} then={<Discover />} />
{unfinishedTickets.map((ticket: any) => (
<TicketPipeline key={ticket.id} ticket={ticket} />
))}
</Sequence>
</Workflow>
);
});
Agents: agents.ts
// scripts/worktree-feature/agents.ts
import { ToolLoopAgent as Agent, stepCountIs } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";
import { ClaudeCodeAgent, CodexAgent } from "smithers-orchestrator";
import { SYSTEM_PROMPT } from "./system-prompt";
const USE_CLI = process.env.USE_CLI_AGENTS !== "0";
const UNSAFE = process.env.SMITHERS_UNSAFE === "1";
// Claude: switches between API agent and CLI agent
const claudeApi = new Agent({
model: anthropic("claude-fable-5"),
instructions: SYSTEM_PROMPT,
stopWhen: stepCountIs(100),
});
const claudeCli = new ClaudeCodeAgent({
model: "claude-fable-5",
systemPrompt: SYSTEM_PROMPT,
dangerouslySkipPermissions: UNSAFE,
timeoutMs: 30 * 60 * 1000,
});
export const claude = USE_CLI ? claudeCli : claudeApi;
// Codex: CLI agent (CodexAgent does not have an API mode)
export const codex = new CodexAgent({
model: "gpt-5.5",
systemPrompt: SYSTEM_PROMPT,
yolo: UNSAFE,
timeoutMs: 30 * 60 * 1000,
});
Validation Loop: ValidationLoop.tsx
// scripts/worktree-feature/components/ValidationLoop.tsx
import { Loop, Sequence } from "smithers-orchestrator";
import { Implement } from "./Implement";
import { Validate } from "./Validate";
import { Review } from "./Review";
import { ReviewFix } from "./ReviewFix";
import { useCtx } from "../smithers";
const MAX_REVIEW_ROUNDS = 3;
export function ValidationLoop({ ticket }: { ticket: { id: string } }) {
const ctx = useCtx();
const ticketId = ticket.id;
const claudeReview = ctx.latest("review", `${ticketId}:review-claude`);
const codexReview = ctx.latest("review", `${ticketId}:review-codex`);
const allApproved = !!claudeReview?.approved && !!codexReview?.approved;
return (
<Loop
id={`${ticketId}:impl-review-loop`}
until={allApproved}
maxIterations={MAX_REVIEW_ROUNDS}
onMaxReached="return-last"
>
<Sequence>
<Implement ticket={ticket} />
<Validate ticket={ticket} />
<Review ticket={ticket} />
<ReviewFix ticket={ticket} />
</Sequence>
</Loop>
);
}
Parallel Review: Review.tsx
// scripts/worktree-feature/components/Review.tsx
import { Parallel } from "smithers-orchestrator";
import { Task, useCtx, outputs } from "../smithers";
import { claude, codex } from "../agents";
import ReviewPrompt from "./Review.mdx";
export function Review({ ticket }: { ticket: { id: string; title: string } }) {
const ctx = useCtx();
const ticketId = ticket.id;
const latestValidate = ctx.latest("validate", `${ticketId}:validate`);
if (!latestValidate?.allPassed) return null;
return (
<Parallel>
<Task
id={`${ticketId}:review-claude`}
output={outputs.review}
agent={claude}
timeoutMs={15 * 60 * 1000}
continueOnFail
>
<ReviewPrompt ticketId={ticketId} reviewer="claude" />
</Task>
<Task
id={`${ticketId}:review-codex`}
output={outputs.review}
agent={codex}
timeoutMs={15 * 60 * 1000}
continueOnFail
>
<ReviewPrompt ticketId={ticketId} reviewer="codex" />
</Task>
</Parallel>
);
}
Ticket Pipeline: TicketPipeline.tsx
// scripts/worktree-feature/components/TicketPipeline.tsx
import { Sequence } from "smithers-orchestrator";
import { ValidationLoop } from "./ValidationLoop";
import { Report } from "./Report";
import { useCtx } from "../smithers";
export function TicketPipeline({ ticket }: { ticket: { id: string } }) {
const ctx = useCtx();
const latestReport = ctx.latest("report", `${ticket.id}:report`);
const ticketComplete = latestReport != null;
return (
<Sequence key={ticket.id} skipIf={ticketComplete}>
<ValidationLoop ticket={ticket} />
<Report ticket={ticket} />
</Sequence>
);
}
Running
cd scripts/worktree-feature
bun install
./run.sh
Key Patterns
createSmithers registers 6 output schemas; generates typed tables, outputs, and Task components.
ClaudeCodeAgent / CodexAgent run real CLI tools with full filesystem access.
<Loop> iterates implement/validate/review/fix until both reviewers approve or MAX_REVIEW_ROUNDS exhausted.
<Parallel> runs dual review simultaneously; both must approve.
ctx.latest(schemaKey, nodeId) reads the highest-iteration output for a task.
- MDX prompts —
.mdx files serve as prompt templates with JSX interpolation.
skipIf skips already-completed tickets on resume.
continueOnFail prevents a single review failure from blocking the pipeline.
- Dynamic ticket mapping —
unfinishedTickets.map() renders one TicketPipeline per ticket.