Skip to main content
The implement-review loop is the most important pattern in Smithers. It wraps implementation in a Ralph loop with validation and multi-agent review, catching issues early and forcing agents to fix them before moving on. This pattern is strongly recommended for any workflow that produces code or other artifacts that need quality assurance.

Overview

The loop runs four steps per iteration:
  1. Implement — An agent writes code (preferably Codex).
  2. Validate — An agent runs tests independently to verify correctness.
  3. Review — Two agents review the code in parallel (Claude + Codex).
  4. ReviewFix — An agent addresses all review issues.
The loop repeats until both reviewers approve or maxIterations is hit.

Minimal Example

import { createSmithers, Task, Sequence, Parallel, Ralph } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, tables } = createSmithers({
  implement: z.object({
    summary: z.string(),
    filesChanged: z.array(z.string()),
    allTestsPassing: z.boolean(),
  }),
  validate: z.object({
    allPassed: z.boolean(),
    failingSummary: z.string().nullable(),
  }),
  review: z.object({
    reviewer: z.string(),
    approved: z.boolean(),
    issues: z.array(z.object({
      severity: z.enum(["critical", "major", "minor", "nit"]),
      file: z.string(),
      description: z.string(),
    })),
    feedback: z.string(),
  }),
  reviewFix: z.object({
    fixesMade: z.array(z.object({ issue: z.string(), fix: z.string() })),
    allIssuesResolved: z.boolean(),
  }),
});

The ValidationLoop Component

This is the core pattern. Each step is its own component with an MDX prompt and Zod schema:
// components/ValidationLoop.tsx
import { Ralph, Sequence } from "smithers-orchestrator";
import { Implement } from "./Implement";
import { Validate } from "./Validate";
import { Review } from "./Review";
import { ReviewFix } from "./ReviewFix";
import { useCtx, tables } from "../smithers";
import type { Ticket } from "./Discover.schema";
import type { ReviewOutput } from "./Review.schema";

const MAX_REVIEW_ROUNDS = 3;

export function ValidationLoop({ ticket }: { ticket: Ticket }) {
  const ctx = useCtx();
  const ticketId = ticket.id;

  const claudeReview = ctx.latest(tables.review, `${ticketId}:review-claude`) as ReviewOutput | undefined;
  const codexReview = ctx.latest(tables.review, `${ticketId}:review-codex`) as ReviewOutput | undefined;

  const allApproved = !!claudeReview?.approved && !!codexReview?.approved;

  return (
    <Ralph
      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>
    </Ralph>
  );
}

Parallel Multi-Agent Review

The Review component runs Claude and Codex in parallel. Using two different models catches more issues than a single reviewer. Use continueOnFail so one reviewer timing out does not block the other:
// components/Review.tsx
import { Parallel } from "smithers-orchestrator";
import { Task, useCtx, tables } from "../smithers";
import { claude, codex } from "../agents";
import ReviewPrompt from "./Review.mdx";
import type { Ticket } from "./Discover.schema";
import type { ValidateOutput } from "./Validate.schema";

export function Review({ ticket }: { ticket: Ticket }) {
  const ctx = useCtx();
  const ticketId = ticket.id;
  const latestValidate = ctx.latest(tables.validate, `${ticketId}:validate`) as ValidateOutput | undefined;

  // Skip review if tests fail -- send back to Implement
  if (!latestValidate?.allPassed) return null;

  const reviewProps = {
    ticketId,
    ticketTitle: ticket.title,
    ticketDescription: ticket.description,
  };

  return (
    <Parallel>
      <Task
        id={`${ticketId}:review-claude`}
        output={tables.review}
        agent={claude}
        timeoutMs={15 * 60 * 1000}
        continueOnFail
      >
        <ReviewPrompt {...reviewProps} reviewer="claude" />
      </Task>
      <Task
        id={`${ticketId}:review-codex`}
        output={tables.review}
        agent={codex}
        timeoutMs={15 * 60 * 1000}
        continueOnFail
      >
        <ReviewPrompt {...reviewProps} reviewer="codex" />
      </Task>
    </Parallel>
  );
}

Feeding Review Feedback Back to Implement

On subsequent iterations, the Implement component reads previous review issues and validation failures, then passes them to the agent so it knows what to fix:
// components/Implement.tsx
import { Task, useCtx, tables } from "../smithers";
import { codex } from "../agents";
import ImplementPrompt from "./Implement.mdx";
import type { Ticket } from "./Discover.schema";

export function Implement({ ticket }: { ticket: Ticket }) {
  const ctx = useCtx();
  const ticketId = ticket.id;

  const latestImplement = ctx.latest(tables.implement, `${ticketId}:implement`);
  const latestValidate = ctx.latest(tables.validate, `${ticketId}:validate`);
  const claudeReview = ctx.latest(tables.review, `${ticketId}:review-claude`);
  const codexReview = ctx.latest(tables.review, `${ticketId}:review-codex`);

  const reviewIssues = [
    ...(claudeReview?.issues ?? []),
    ...(codexReview?.issues ?? []),
  ];

  return (
    <Task id={`${ticketId}:implement`} output={tables.implement} agent={codex} timeoutMs={45 * 60 * 1000}>
      <ImplementPrompt
        ticketId={ticketId}
        ticketTitle={ticket.title}
        ticketDescription={ticket.description}
        previousImplementation={latestImplement ?? null}
        validationFeedback={latestValidate ?? null}
        reviewFixes={reviewIssues.length > 0 ? JSON.stringify(reviewIssues, null, 2) : null}
      />
    </Task>
  );
}

ReviewFix with skipIf

The ReviewFix component only runs if there are actual issues to fix. If both reviewers approved, it is skipped entirely:
// components/ReviewFix.tsx
import { Task, useCtx, tables } from "../smithers";
import { codex } from "../agents";
import ReviewFixPrompt from "./ReviewFix.mdx";
import type { Ticket } from "./Discover.schema";

export function ReviewFix({ ticket }: { ticket: Ticket }) {
  const ctx = useCtx();
  const ticketId = ticket.id;

  const claudeReview = ctx.latest(tables.review, `${ticketId}:review-claude`);
  const codexReview = ctx.latest(tables.review, `${ticketId}:review-codex`);

  const allApproved = !!claudeReview?.approved && !!codexReview?.approved;
  const allIssues = [...(claudeReview?.issues ?? []), ...(codexReview?.issues ?? [])];

  return (
    <Task
      id={`${ticketId}:review-fix`}
      output={tables.reviewFix}
      agent={codex}
      skipIf={allApproved || allIssues.length === 0}
    >
      <ReviewFixPrompt
        ticketId={ticketId}
        issues={allIssues}
        feedback={[claudeReview?.feedback, codexReview?.feedback].filter(Boolean).join("\n\n")}
      />
    </Task>
  );
}

Why This Pattern Works

  • Validation before review — No point reviewing code that does not compile or pass tests. If validation fails, the loop skips review and goes straight back to implement.
  • Parallel review — Two different models catch different kinds of issues. Claude is strong on architecture and logic; Codex is strong on code correctness and edge cases.
  • Structured issues — Review output uses a typed issues array with severity, file, and description. This lets ReviewFix address each issue systematically.
  • Bounded iterationsmaxIterations prevents infinite loops. Use onMaxReached: "return-last" to accept the best effort after the cap.
  • Resumable — Every step persists to SQLite. If the workflow crashes mid-loop, it resumes from the last incomplete task.

Next Steps