JuliusBrussee/caveman · error · Error

caveman build: config must export defineBuild()

Error message

caveman build: config must export defineBuild()

What it means

During build, the CLI fresh-imports caveman.config.ts and expects a default or named `config` export shaped by defineBuild(); the sanity check requires config to be truthy and config.evals to be a string. Failing either means the module didn't export a defineBuild() result (default export missing, wrong export name, or the file exports something else), and the throw names the expected export.

Source

Thrown at packages/agent/src/cli.ts:676

  loaded: Awaited<ReturnType<typeof loadDevModule>>,
): Promise<boolean> {
  const lockPath = ".caveman/agent.lock.json";
  try {
    await readFile(resolve(root, lockPath));
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
    throw error;
  }
  const configPath = "caveman.config.ts";
  await loaded.includeFiles([lockPath]);
  await loaded.includeSourceGraph([configPath]);
  const imported = await importFresh(resolve(loaded.rootDir, configPath)) as {
    default?: BuildConfig;
    config?: BuildConfig;
  };
  const config = imported.default ?? imported.config;
  if (!config || typeof config.evals !== "string") {
    throw new Error("caveman build: config must export defineBuild()");
  }
  const evalFiles: string[] = [];
  for await (const path of glob(config.evals, { cwd: root })) {
    evalFiles.push(resolve(root, path));
  }
  await loaded.includeSourceGraph(evalFiles);
  const sourceFiles: string[] = [];
  for (const pattern of SOURCE_PATTERNS) {
    for await (const path of glob(pattern, { cwd: root })) sourceFiles.push(resolve(root, path));
  }
  await loaded.includeFiles(sourceFiles);
  await loaded.includeOptionalFiles(PACKAGE_STATE_FILES);
  return true;
}

function firstUsefulError(error: unknown): string {
  const stack = error instanceof Error ? error.stack ?? error.message : String(error);
  return stack.split("\n").slice(0, 4).join("\n");

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Write the config as: import { defineBuild } from "…"; export default defineBuild({ evals: "evals/**/*.ts", … });
  2. Ensure the `evals` field is a string glob — it is the one field checked, so omitting it triggers the same error as no export.
  3. Confirm the file is exactly caveman.config.ts at the repo root (the loader imports that fixed path).

Example fix

// before: caveman.config.ts
export const cavemanConfig = { evals: "evals/**" };

// after
import { defineBuild } from "@caveman/agent";
export default defineBuild({ evals: "evals/**/*.ts" });
Defensive patterns

Strategy: type-guard

Validate before calling

import { pathToFileURL } from "node:url";
async function configExportsDefineBuild(root: string): Promise<boolean> {
  const mod = await import(`${pathToFileURL(join(root, "caveman.config.ts")).href}?t=${Date.now()}`);
  const config = mod.default ?? mod.config;
  return Boolean(config) && typeof config.evals === "string";
}

Type guard

interface BuildConfigShape { evals: string }
function isBuildConfig(value: unknown): value is BuildConfigShape {
  return typeof value === "object" && value !== null
    && typeof (value as { evals?: unknown }).evals === "string";
}

Prevention

When it happens

Trigger: caveman.config.ts with `export const config = defineBuild({...})` where evals is missing/not a string; exporting under a different name (`export const myConfig`); forgetting to call defineBuild and exporting a plain object without an `evals: string` glob; syntax that makes importFresh resolve to an empty module namespace.

Common situations: First-time config authoring; refactoring the config and dropping the `export default`; copy-pasting a config example that predates defineBuild; circular import making the default undefined at evaluation time.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/620178eb66e7074c. Report an issue: GitHub.