{"record":{"id":"194ac2816b0bf206","repo":"JuliusBrussee/caveman","slug":"tools-must-be-unique-caveman-names","errorCode":null,"errorMessage":"tools must be unique caveman_* names","messagePattern":"tools must be unique caveman_\\* names","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"skills/verbs-gate.mjs","lineNumber":81,"sourceCode":"  const cloudVerbs = discoveryVerbs(indexSrc, \"const CLOUD_DISCOVERY\");\n  if (toolVerbs.size === 0) errors.push(\"could not parse TOOL_DISCOVERY verbs from CLI index.ts\");\n  if (cloudVerbs.size === 0) errors.push(\"could not parse CLOUD_DISCOVERY verbs from CLI index.ts\");\n\n  // MCP tool names: engine server (Go) + agent-native server (TS).\n  const mcpTools = new Set();\n  let declaredEngineTools = [];\n  try {\n    const manifest = JSON.parse(readFileSync(join(skillsDir, \"engine-mcp-tools.json\"), \"utf8\"));\n    if (manifest.schema_version !== \"1\" || !Array.isArray(manifest.tools)) {\n      throw new Error('schema_version must be \"1\" and tools must be an array');\n    }\n    declaredEngineTools = [...manifest.tools];\n    if (\n      declaredEngineTools.length === 0\n      || new Set(declaredEngineTools).size !== declaredEngineTools.length\n      || declaredEngineTools.some((name) => typeof name !== \"string\" || !/^caveman_[a-z_]+$/.test(name))\n    ) {\n      throw new Error(\"tools must be unique caveman_* names\");\n    }\n    for (const name of declaredEngineTools) mcpTools.add(name);\n  } catch (error) {\n    errors.push(`engine-mcp-tools.json: ${error.message}`);\n  }\n  const engineSrc = readOptional(join(skillsDir, \"..\", \"mcp\", \"engine_tools.go\"));\n  const sourceEngineTools = [...engineSrc.matchAll(/Tool\\w+\\s*=\\s*\"(caveman_[a-z_]+)\"/g)].map((match) => match[1]);\n  if (sourceEngineTools.length > 0) {\n    const declared = [...declaredEngineTools].sort();\n    const source = [...new Set(sourceEngineTools)].sort();\n    if (JSON.stringify(declared) !== JSON.stringify(source)) {\n      errors.push(\"engine-mcp-tools.json drifted from public/mcp/engine_tools.go\");\n    }\n  }\n  const agentMcpSrc = readOptional(join(cliDir, \"src\", \"agent-mcp.ts\"));\n  for (const m of agentMcpSrc.matchAll(/name:\\s*\"(caveman_[a-z_]+)\"/g)) mcpTools.add(m[1]);\n  if (mcpTools.size === 0) errors.push(\"could not parse MCP tool names\");\n","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/skills/verbs-gate.mjs#L63-L99","documentation":"Thrown inside loadSurfaces() in skills/verbs-gate.mjs when the engine-mcp-tools.json tools array is non-empty-safe: it rejects an empty array, duplicate names, or any entry that is not a string matching /^caveman_[a-z_]+$/. These rules keep the MCP tool namespace uniform (lowercase snake-case prefixed with caveman_) so verb collision checks downstream are reliable. The throw is caught and reported as `engine-mcp-tools.json: tools must be unique caveman_* names` in the gate error list.","triggerScenarios":"A manifest with zero tools; the same name listed twice after a merge; a name like \"Compress\", \"caveman-compress\", \"caveman_WebSearch\", or a non-string entry (number/object).","commonSituations":"Merge conflicts duplicating entries; hand-adding a tool with camelCase or a hyphen; generator emitting the raw tool name without the caveman_ prefix.","solutions":["Make every entry a unique string matching caveman_[a-z_]+ (e.g. \"caveman_tool_search\") and ensure at least one entry exists.","Remove duplicates introduced by merges; sort the array to make future duplicates obvious.","Cross-check against public/mcp/engine_tools.go — the gate compares the manifest against `Tool\\w+ = \"caveman_...\"` constants and reports drift."],"exampleFix":"// before\n{ \"schema_version\": \"1\", \"tools\": [\"caveman_compress\", \"caveman_compress\", \"Compress\"] }\n// after\n{ \"schema_version\": \"1\", \"tools\": [\"caveman_compress\", \"caveman_expand\"] }","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function hasValidToolNames(doc) {\n  return Array.isArray(doc.tools) && doc.tools.length > 0\n    && new Set(doc.tools).size === doc.tools.length\n    && doc.tools.every((n) => typeof n === \"string\" && /^caveman_[a-z_]+$/.test(n));\n}","tryCatchPattern":null,"preventionTips":["Name MCP tools caveman_ + lowercase snake_case from the start.","After merges, sort and de-duplicate the tools array before committing; the gate also diffs it against public/mcp/engine_tools.go."],"tags":["json","validation","mcp","naming","gate"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}