---
title: Test and review
description: Check a candidate against the original issue, plan, implementation, and previous findings.
---

Review starts with context. The reviewer reads the original issue,
acceptance criteria, plan, implementation explanation, previous attempts,
and test output. It then examines the exact candidate commit. A candidate
is the committed revision proposed for this issue, identified by its SHA.

## Check engineering decisions

The review prompt and selected skills should name the project's expectations.
"Review this code" leaves too much unstated. Ask for evidence against the
accepted scope, including the reasons behind the first implementation.

To use an approved project skill, import `skills` from `@foundry.sh/marbles`
and add `skills: skills.none().add("./skills/review")` to the reviewer
declaration. That directory must contain `SKILL.md`. Select skills for the
review's remit and retain their version with the evidence; use
[skill evaluations](/marbles/use-cases/evaluations/skills) before changing them.

| Decision | What to check |
| --- | --- |
| Add a dependency | Is it needed? Does the repository already supply it? Check its version, lockfile, license, maintenance status, and transitive dependencies. |
| Create a package | Is there a real owner and reusable responsibility? Check its exports, dependency direction, build, and tests. A folder alone may be enough. |
| Change a module | Does its interface keep complexity out of callers? Include ordering rules, errors, and compatibility in the review. |
| Change domain rules | Do names match the domain? Are invariants enforced where the state changes? |
| Add tests | Do they exercise the required result through the module's interface, including failure cases? Do they fail for the defect being fixed? |

Check the plan's promised commands against the actual evidence. The example
below runs installation and `bun test`; add the project's lint, typecheck,
build, or other required checks before relying on it for delivery. Missing
browser or service evidence stays missing even when a model says the code
looks correct.

## Run the verifier

Create `.foundry/marbles/_factory/verify.ts`:

<details>
<summary>Complete verify.ts</summary>

```ts
import { agent, Output, step } from "@foundry.sh/marbles";
import {
  chronicle,
  recordEvent,
  recordSchema,
  requireRecord,
  reviewSchema,
  workInput,
} from "./record";

const reviewer = agent({
  prompt:
    "Review the exact candidate commit against the original issue, acceptance criteria, plan, implementation explanation, and prior findings. Read repository instructions and relevant callers. Check the code yourself. Examine new packages and dependencies for necessity, ownership, version and lockfile consistency. Check module interfaces, domain invariants, failure handling, and whether tests exercise the required results. For each criterion, name concrete evidence or mark it unmet. Mark promised but unperformed checks as missing evidence. Each blocker needs a file or record reference, a triggering condition, and a consequence. Separate defects from preferences and identify findings the revision already resolved. Do not invent requirements. Do not edit or commit.",
});
export const verify = step("factory-verify")
  .input(workInput)
  .output(recordSchema)
  .do(async ({ input: { work }, agents, workspaces, exec, run }) => {
    const { change } = work;
    if (
      change &&
      work.verification?.revision === change.revision &&
      work.status !== "implemented"
    ) {
      return work;
    }
    if (!change || work.status !== "implemented") {
      throw new Error("An implemented candidate is required.");
    }
    try {
      return await workspaces.current.git.withWorktree(
        { base: change.revision },
        async () => {
          const install = await exec(["bun", "install", "--frozen-lockfile"], {
            check: false,
          });
          const tests =
            install.exitCode === 0
              ? await exec(["bun", "test"], { check: false })
              : {
                  exitCode: -1,
                  stderr: "Tests not run because installation failed.",
                  stdout: "",
                };
          const output = JSON.stringify({ install, tests }, null, 2);
          const checked = await recordEvent(
            work,
            run.id,
            "test output",
            output,
            {
              [`attempts/${work.attempts}/tests.json`]: output,
            }
          );
          const session = await agents.session(
            reviewer,
            work.reviewer ? { session: work.reviewer } : {}
          );
          const reply = await session.generate(
            [
              work.issue.body,
              chronicle(checked),
              `Review ${work.base}..${change.revision}. Initial implementation and all later attempts are in the history.`,
              `Implementation explanation: ${change.summary}`,
            ].join("\n\n"),
            { output: Output.object({ schema: reviewSchema }) }
          );
          const review = reviewSchema.parse(reply.output);
          const head = (await exec(["git", "rev-parse", "HEAD"])).stdout.trim();
          const dirty = (
            await exec(["git", "status", "--porcelain"])
          ).stdout.trim();
          if (head !== change.revision || dirty) {
            throw new Error(
              "Verification changed the checkout. Its evidence cannot be accepted."
            );
          }
          const testsPassed = install.exitCode === 0 && tests.exitCode === 0;
          const meetsCriteria = work.acceptance.every((criterion) =>
            review.criteria.some(
              (item) =>
                item.criterion === criterion && item.met && item.evidence.trim()
            )
          );
          return recordEvent(
            {
              ...checked,
              reviewer: session.ref,
              status:
                testsPassed && meetsCriteria && review.blockers.length === 0
                  ? "verified"
                  : "needs-changes",
              verification: { review, revision: change.revision, testsPassed },
            },
            run.id,
            "review",
            JSON.stringify(review, null, 2)
          );
        }
      );
    } catch (error) {
      // Read the latest record so a failure after testing keeps its output.
      const latest = await requireRecord(work.issue.number);
      await recordEvent(
        { ...latest, status: "blocked" },
        run.id,
        "verification failed",
        String(error)
      );
      throw error;
    }
  });
```

</details>

Replace `factory.ts` with:

```ts
import { workflow } from "@foundry.sh/marbles";
import { implement } from "./_factory/implement";
import { intake } from "./_factory/intake";
import { plan } from "./_factory/plan";
import { issueInput } from "./_factory/record";
import { verify } from "./_factory/verify";

workflow("factory")
  .input(issueInput)
  .do(({ input }) =>
    verify({ work: implement({ work: plan({ work: intake({}, input) }) }) })
  );
```

## Read the result

The verifier records install and test output even when a command fails.
It returns `verified` only when those commands pass, the review supplies
positive evidence for every criterion, and there are no review blockers.
Otherwise it returns `needs-changes`. That run completed its assessment;
it did not approve the code.

A model's assessment is evidence for the human decision. Adapt the commands
to the project's actual checks. A successful `bun test` cannot prove a
browser interaction that the suite never exercises.

For several reviewers, use the [PR review process](/marbles/use-cases/software-factory/maintenance/pull-requests)
to assign different risks and synthesize their findings. This sample has one
reviewer. Adding more sessions without a synthesis step only adds more output.

## Keep review history

This is the persistent reviewer inside the factory. The work log stores
one review session reference per issue, so later attempts can revisit
unresolved findings. Each turn also receives the written history, including
the initial implementation. The transcript is not the only record.

Changing the checkout during verification rejects the evidence. Prompts
ask the agent to leave files alone; they do not enforce read-only access.
Use the [session controls](/marbles/reference/agent-sessions) appropriate
to your environment.

Next: [Revise the candidate](/marbles/use-cases/software-factory/development/revise).
