# Plan, build, review

An agent asks what it needs to know, proposes a plan the person accepts or sends back, carries out the steps with a live log, asking before any step that changes something, and ends with a ship review the person accepts or sends back.

- Registry name: `plan-build-review` (`@candor/plan-build-review`)
- Tier: Premium. Installing it needs a Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/plan-build-review

## Install

```bash
npx candor-ui add plan-build-review
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/plan-build-review`.
It brings in: button, card-registry, chat, clarifying-questions, composer, entity-chip, log-stream, message, plan, prompt-suggestions, ship-review, standard-cards, status, thread, use-candor-chat, work.
npm packages: @ai-sdk/anthropic@^4.0.96, @ai-sdk/react@^4.0.166, ai@^7.0.162, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7.

It installs under your app's `app/` folder and runs with a server and an agent: set `ANTHROPIC_API_KEY`, and read the comment at the top of its `store.ts` for storage and auth. Its files:

- `app/plan-build-review/agent.ts`
- `app/plan-build-review/api/route.ts`
- `app/plan-build-review/api/work/route.ts`
- `app/plan-build-review/db/connect.ts`
- `app/plan-build-review/db/drizzle-repo.ts`
- `app/plan-build-review/db/schema.ts`
- `app/plan-build-review/db/seed.ts`
- `app/plan-build-review/page.tsx`
- `app/plan-build-review/repo.ts`
- `app/plan-build-review/store.ts`
- `app/plan-build-review/ui.tsx`

## <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.

Props:

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

## Functions

### connectBuildDb

```ts
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

```ts
drizzleBuildRepo(db: BuildDb)
```

### seedBuild

```ts
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

```ts
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

```ts
setBuildRepo(repo: BuildRepo)
```

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

### buildRepo

```ts
buildRepo()
```

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

### viewerOf

```ts
viewerOf(request: Request)
```

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

### inspect

```ts
inspect(target: string)
```

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

### askQuestions

```ts
askQuestions(questions: ClarifyingQuestion[])
```

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

### answerQuestions

```ts
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

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

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

### decidePlan

```ts
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

```ts
stepWrites(planId: string, stepId: string)
```

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

### runStep

```ts
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

```ts
requestShipReview(planId: string)
```

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

### ship

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

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

### requestChanges

```ts
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

```ts
workView()
```

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

### planCard

```ts
planCard(plan: Plan)
```

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

### resetStore

```ts
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

```ts
buildToolTitle(tool: string, input: unknown)
```

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

## Types

### BuildDb

```ts
PgDatabase<PgQueryResultHKT, any>
```

### ServiceRecord

A service's builds on Production and Staging, its test suite and its settings.

```ts
{ name: string; label: string; repo: string; production: number; staging: number; tests: number; settings: Record<string, string> }
```

### FlagRecord

```ts
{ name: string; production: boolean; staging: boolean }
```

### QuestionSetRecord

```ts
{ id: string; questions: ClarifyingQuestion[]; askedAt: string; state: "open" | "answered" | "superseded"; answers?: ClarifyingAnswer[] | undefined }
```

### StepRecord

```ts
{ 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

```ts
{ 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

```ts
{ 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

```ts
Omit<PlanRecord, "id" | "rev">
```

### PlanPatch

```ts
Partial<Omit<PlanRecord, "id" | "rev" | "createdAt">>
```

### NewQuestionSetRecord

```ts
Omit<QuestionSetRecord, "id" | "state" | "answers">
```

### Action

```ts
(typeof actions)[number]
```

### ShipCheckData

```ts
{ id: string; label: string; state: "pass" | "fail" | "running" | "pending"; detail?: string | undefined }
```

### WorkView

```ts
{ 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.

```ts
{ 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 }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
