JuliusBrussee/caveman · error

verbs is not an array

Error message

verbs is not an array

What it means

Thrown inside loadSurfaces() in skills/verbs-gate.mjs while reading agents/reserved-verbs.json, the canonical registry of top-level CLI verbs. The code requires the parsed document to have a `verbs` array; a missing, null, or differently-shaped `verbs` field throws. The throw is caught immediately and recorded as `reserved-verbs.json: <message>` in the accumulated gate errors, so it surfaces as part of the verbs-gate failure report rather than a crash.

Source

Thrown at skills/verbs-gate.mjs:54

  return verbs;
}

// Derive every surface a skill may reference. `errors` is non-empty when a
// source could not be parsed — the caller must fail closed rather than scan
// against an empty surface (which would pass everything).
export function loadSurfaces({ skillsDir, cliDir }) {
  const errors = [];
  const publicRoot = join(skillsDir, "..");
  const packageRoot = existsSync(join(publicRoot, "sdk"))
    ? publicRoot
    : join(publicRoot, "packages");

  // Top-level CLI verbs: the reserved-verbs registry is the canonical source
  // (it already drives reserved-verbs.generated.ts).
  let topLevel = new Set();
  try {
    const doc = JSON.parse(readFileSync(join(skillsDir, "..", "agents", "reserved-verbs.json"), "utf8"));
    if (!Array.isArray(doc.verbs)) throw new Error("verbs is not an array");
    topLevel = new Set(doc.verbs);
  } catch (error) {
    errors.push(`reserved-verbs.json: ${error.message}`);
  }

  // `caveman tools <verb>` / `caveman cloud <verb>` namespaces.
  const indexSrc = readOptional(join(cliDir, "src", "index.ts"));
  const toolVerbs = discoveryVerbs(indexSrc, "const TOOL_DISCOVERY");
  const cloudVerbs = discoveryVerbs(indexSrc, "const CLOUD_DISCOVERY");
  if (toolVerbs.size === 0) errors.push("could not parse TOOL_DISCOVERY verbs from CLI index.ts");
  if (cloudVerbs.size === 0) errors.push("could not parse CLOUD_DISCOVERY verbs from CLI index.ts");

  // MCP tool names: engine server (Go) + agent-native server (TS).
  const mcpTools = new Set();
  let declaredEngineTools = [];
  try {
    const manifest = JSON.parse(readFileSync(join(skillsDir, "engine-mcp-tools.json"), "utf8"));
    if (manifest.schema_version !== "1" || !Array.isArray(manifest.tools)) {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Open agents/resolved path reserved-verbs.json and restore `verbs` as a JSON array of verb strings: `{ "verbs": ["install", "run", ...] }`.
  2. Check the gate output for the companion errors — reserved-verbs drift usually co-occurs with generated-file drift (reserved-verbs.generated.ts).
  3. Re-run the verbs gate to confirm the file passes.

Example fix

// before: agents/reserved-verbs.json
{ "verbs": { "install": true } }
// after
{ "verbs": ["install", "run", "wrap"] }
Defensive patterns

Strategy: type-guard

Type guard

function isReservedVerbsDoc(doc) {
  return doc !== null && typeof doc === "object" && Array.isArray(doc.verbs)
    && doc.verbs.every((v) => typeof v === "string");
}

Prevention

When it happens

Trigger: reserved-verbs.json exists and parses as JSON but `verbs` is an object, a string, or absent (e.g. someone restructured the file into {version, verbs: {...}} or renamed the key). A pure JSON syntax error takes the same path with a different message.

Common situations: Editing the reserved-verbs registry and changing its shape; tooling that pretty-prints/rewrites the file and drops or renames the array; schema drift after an upgrade.

Related errors


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