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
- Open agents/resolved path reserved-verbs.json and restore `verbs` as a JSON array of verb strings: `{ "verbs": ["install", "run", ...] }`.
- Check the gate output for the companion errors — reserved-verbs drift usually co-occurs with generated-file drift (reserved-verbs.generated.ts).
- 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
- Never restructure reserved-verbs.json without updating skills/verbs-gate.mjs in the same change.
- Run the verbs gate in pre-commit so registry shape errors surface immediately.
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
- schema_version must be "1" and tools must be an array
- tools must be unique caveman_* names
- cave_eve_terminal_${result.status}
- ${key} is required
- --provider requires a value
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/082abe3701fb85b7.
Report an issue: GitHub.