{"record":{"id":"4e53a59584e35415","repo":"EveryInc/compound-engineering-plugin","slug":"skipping-agent-agent-name-sanitized-name","errorCode":null,"errorMessage":"Skipping agent \"${agent.name}\": sanitized name \"${safeName}\" collides with another agent","messagePattern":"Skipping agent \"(.+?)\": sanitized name \"(.+?)\" collides with another agent","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/targets/opencode.ts","lineNumber":138,"sourceCode":"  if (!skillsEscaped) await cleanupRemovedManagedDirectories(openCodePaths.skillsDir, manifest, \"skills\", currentSkills)\n\n  const hadExistingConfig = await pathExists(openCodePaths.configPath)\n  const backupPath = await backupFile(openCodePaths.configPath)\n  if (backupPath) {\n    console.log(`Backed up existing config to ${backupPath}`)\n  }\n  const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config)\n  await writeJson(openCodePaths.configPath, merged)\n  if (hadExistingConfig) {\n    console.log(\"Merged plugin config into existing opencode.json (user settings preserved)\")\n  }\n\n  const seenAgents = new Set<string>()\n  const preservedAgentNames = new Set<string>()\n  for (const agent of bundle.agents) {\n    const safeName = sanitizePathName(agent.name)\n    if (seenAgents.has(safeName)) {\n      console.warn(`Skipping agent \"${agent.name}\": sanitized name \"${safeName}\" collides with another agent`)\n      continue\n    }\n    seenAgents.add(safeName)\n    const agentFileName = `${safeName}.md`\n    if (agentsEscaped) {\n      preservedAgentNames.add(agentFileName)\n      continue\n    }\n    const targetPath = path.join(openCodePaths.agentsDir, agentFileName)\n    const preserved = await cleanupCurrentManagedFile(targetPath, manifest, \"agents\", agentFileName)\n    if (preserved) {\n      preservedAgentNames.add(agentFileName)\n      continue\n    }\n    await writeText(targetPath, agent.content + \"\\n\")\n  }\n\n  const preservedCommandNames = new Set<string>()","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/EveryInc/compound-engineering-plugin/blob/c9c10f8c75412c7232cb2bd663e5fd1cea98d84e/src/targets/opencode.ts#L120-L156","documentation":"writeOpenCodeBundle in src/targets/opencode.ts sanitizes each agent's name into a filename via sanitizePathName. If two distinct agent names sanitize to the same filename (e.g. 'Code Review' and 'code_review' both -> 'code-review'), the second is skipped with this warning and its agent file is not written, since one file cannot hold both.","triggerScenarios":"A bundle.agents array contains two agents whose names normalize to the same safeName — differing only by case, whitespace, underscores/dashes, or other characters stripped by sanitizePathName. The second occurrence in iteration order triggers the warning and `continue`.","commonSituations":"Plugin authors defining agents whose names differ only in case or punctuation; merging multiple plugins whose agents share a name; renaming an agent without removing the old definition so both 'OldName' and 'old-name' exist.","solutions":["Rename one of the colliding agents in its definition so the sanitized names differ (e.g. 'CodeReview' vs 'code-review-helper').","Check the source plugin/manifest for duplicate agent definitions and remove the stale one.","If collisions come from merging bundles, scope one set of agents into a different directory or prefix their names.","Re-run the install and confirm the warning is gone and both agent files exist."],"exampleFix":"// before: two agents colliding\n{ \"name\": \"Code Review\" }\n{ \"name\": \"code_review\" }   // both sanitize to code-review\n// after\n{ \"name\": \"Code Review\" }\n{ \"name\": \"Code Reviewer\" } // distinct sanitized names","handlingStrategy":"validation","validationCode":"function findSanitizedCollisions(agents: { name: string }[], sanitize: (s: string) => string): string[] {\n  const seen = new Map<string, string[]>()\n  for (const a of agents) {\n    const k = sanitize(a.name)\n    seen.set(k, [...(seen.get(k) ?? []), a.name])\n  }\n  return [...seen.entries()].filter(([, v]) => v.length > 1).map(([k, v]) => `${k}: ${v.join(', ')}`)\n}\n// run before writing the bundle; empty array means no collisions","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Give agents names that stay distinct after lowercasing and stripping punctuation/underscores","Lint plugin definitions for duplicate or near-duplicate agent names before release","When merging bundles, check for cross-bundle name overlaps first"],"tags":["opencode","naming-collision","agents","sanitization"],"backgroundTag":"filename-collision-after-sanitize","analyzedSha":"c9c10f8c75412c7232cb2bd663e5fd1cea98d84e","analyzedAt":"2026-08-31T15:18:07.959Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}