Plan, build, review (Claude Agent SDK)

Premium

The person's answer to the agent's permission request: { id, approved, reason? }.

Markdown

Preview

It runs with a server and an agent, so there’s no preview here: install it to run it in your app.

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/plan-build-review-claude

Brings in: button, card-registry, chat, clarifying-questions, claude-agent-server, composer, entity-chip, log-stream, message, plan, prompt-suggestions, ship-review, standard-cards, status, thread, use-claude-agent, work · npm: @anthropic-ai/claude-agent-sdk@^0.3.318, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7

<POST>

No props.

Also takes the props of Request.

<POST>

The agent, on the Claude Agent SDK. Set ANTHROPIC_API_KEY. Runs on one Node server, not spread over instances.

No props.

Also takes the props of Request.

<GET>

What the work panel shows: open questions, and the latest plan with its live status, log and review.

No props.

<POST>

The person's decisions: answers to the agent's questions, accepting or sending back a plan, and shipping or sending back the review. Each returns message, the words the browser sends the agent so it knows what was decided.

No props.

Also takes the props of Request.

<Page>

No props.

<PlanBuildReview>

A chat with an agent that plans, builds and reviews a change to Acme web. The work panel beside it holds the person's side: the agent's questions, its plan to accept or send back, each step's live status and log, and the ship review at the end.

PropTypeDefaultDescription
chatrequiredBuildChat—The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
workApirequiredstring—The work API route, polled for the plan, the build and the review, e.g. "/plan-build-review/api/work".

Functions

connectBuildDb(url: string)

Open the block's database from a Postgres URL with the postgres-js driver. Swap the driver here (node-postgres, Neon's websocket Pool, …) if your app already uses another; it must support interactive transactions.

drizzleBuildRepo(db: BuildDb)
seedBuild(db: BuildDb)

Load the demo's services and flags into the database, leaving any that already exist alone. Run it once after migrating, e.g. from a script: await seedBuild(connectBuildDb(process.env.DATABASE_URL!)).

memoryBuildRepo()

In this server process's memory: gone on restart and not shared between instances. Transactions run one at a time, and a transaction that throws is rolled back to the state it started from.

setBuildRepo(repo: BuildRepo)

Use your own storage (or a database you opened yourself). Call it once, before the routes run.

buildRepo()

The storage in use: whatever setBuildRepo() was given, else Postgres when DATABASE_URL is set, else memory.

viewerOf(request: Request)

Who is asking. Replace with your auth; the demo reads a cookie (default Riley).

inspect(target: string)

A service's builds and settings, or a flag's state, per environment.

askQuestions(questions: ClarifyingQuestion[])

Record the agent's questions for the person, replacing any still open. Nothing is pre-answered.

answerQuestions(id: string, answers: ClarifyingAnswer[] | null)

The person's answers (or null to leave it to the agent). Returns the message the browser sends to the agent, written by the app from the question and option text, so the agent reads exactly what was chosen.

proposePlan(input: { title: string; steps: { title: string; summary: string; action: Action; target: string }[] })

Propose a plan. Nothing runs until the person accepts it.

decidePlan(id: string, accept: boolean, skipped: string[], viewer: string)

The person accepts the plan (dropping any steps they don't want) or sends it back.

stepWrites(planId: string, stepId: string)

Whether a step changes something, so the routes know when to ask. Unknown steps count as changing.

runStep(planId: string, stepId: string, kind: "read" | "write", actor: string)

Run one step of an accepted plan, in order. kind says which tool asked: run_step may only run steps that read; apply_step only steps that write (and the route asked the person before calling it). The step starts in one transaction (so of two calls at once only one starts it) and finishes in another; no lock is held while it works.

requestShipReview(planId: string)

The agent asks for the person's ship review once the steps are done.

ship(planId: string, reviewId: string, viewer: string)

The person ships what they reviewed, if every check passed and no step ran since.

requestChanges(planId: string, reviewId: string, viewer: string, note?: string)

The person sends the review back. The agent hears what they want changed in their next message.

workView()

The open questions, and the latest plan with its live status. Durations only where the store timed the step.

planCard(plan: Plan)

The plan as the plan card shows it in the chat.

resetStore({ pace = 1 }: { pace?: number | undefined } = {})

For tests: start from the seed data again, optionally with no waiting in steps. On a database this deletes the block's tables' rows.

buildToolTitle(tool: string, input: unknown)

Plain-words titles for the block's tools, the same for both agent SDKs.

Types

BuildDb
PgDatabase<PgQueryResultHKT, any>
ServiceRecord
A service's builds on Production and Staging, its test suite and its settings.
{ name: string; label: string; repo: string; production: number; staging: number; tests: number; settings: Record<string, string> }
FlagRecord
{ name: string; production: boolean; staging: boolean }
QuestionSetRecord
{ id: string; questions: ClarifyingQuestion[]; askedAt: string; state: "open" | "answered" | "superseded"; answers?: ClarifyingAnswer[] | undefined }
StepRecord
{ id: string title: string summary: string action: Action target: string /** The service or flag it acts on, as it was when the plan was proposed. */ touches: EntityRef status: RunStatus startedAt?: string | undefined finishedAt?: string | undefined failure?: string | undefined }
ReviewRecord
{ id: string /** The plan's version when the review was made; shipping checks nothing ran since. */ version: number state: "open" | "shipped" | "changes-requested" createdAt: string decision?: { by: string; at: string; note?: string | undefined } | undefined }
PlanRecord
{ id: string title: string state: "proposed" | "accepted" | "sent-back" steps: StepRecord[] log: LogItem[] /** Bumped every time a step runs, so a review knows if work happened after it. */ version: number /** Builds this plan made, per service, and where they are. */ builds: Record<string, { version: number; commit: string; deployed: boolean }> flags: string[] createdAt: string decision?: { by: s
NewPlanRecord
Omit<PlanRecord, "id" | "rev">
PlanPatch
Partial<Omit<PlanRecord, "id" | "rev" | "createdAt">>
NewQuestionSetRecord
Omit<QuestionSetRecord, "id" | "state" | "answers">
Action
(typeof actions)[number]
ShipCheckData
{ id: string; label: string; state: "pass" | "fail" | "running" | "pending"; detail?: string | undefined }
WorkView
{ questions: { id: string; questions: ClarifyingQuestion[] } | null plan: { id: string title: string state: Plan["state"] steps: PlanStep[] log: LogItem[] running: boolean review?: { id: string; state: Review["state"]; stale: boolean } & ReturnType<typeof reviewOf> | undefined } | null }
BuildChat
What either adapter hook returns (useCandorChat or useClaudeAgent): the block works with both.
{ messages: MessageData[] composer: React.ComponentProps<typeof Composer> approvals: { id: string; tool: string; input?: unknown; title?: string | undefined; reason?: string | undefined }[] respond: (id: string, approved: boolean, reason?: string) => unknown error?: string | undefined }