{"record":{"id":"4041ed1b50887b7a","repo":"bmad-code-org/BMAD-METHOD","slug":"cannot-install-module-resolved-code-skill-di","errorCode":null,"errorMessage":"Cannot install module '${resolved.code}': skill directories '${seenLeaves.get(skillDirName)}' and '${skillPath}' share the leaf name '${skillDirName}' and would overwrite each other. Skill directory names must be unique.","messagePattern":"Cannot install module '(.+?)': skill directories '(.+?)' and '(.+?)' share the leaf name '(.+?)' and would overwrite each other\\. Skill directory names must be unique\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/installer/modules/official-modules.js","lineNumber":349,"sourceCode":"   * at the module root. Shared by both custom marketplace installs\n   * (installFromResolution) and official marketplace-plugin registry installs\n   * (install), so the two paths cannot drift.\n   * @param {Object} resolved - ResolvedModule from PluginResolver\n   * @param {string} targetPath - Destination module directory (e.g. bmadDir/<code>)\n   * @param {Function} fileTrackingCallback - Optional callback to track installed files\n   * @param {Object} moduleConfig - Module configuration passed to copy filtering\n   */\n  async _copyResolvedSkills(resolved, targetPath, fileTrackingCallback = null, moduleConfig = {}) {\n    await fs.ensureDir(targetPath);\n\n    // Copy each skill directory, flattened by leaf name. Leaf names must be\n    // unique — two skills that flatten to the same directory would silently\n    // overwrite each other, so fail loud instead.\n    const seenLeaves = new Map();\n    for (const skillPath of resolved.skillPaths) {\n      const skillDirName = path.basename(skillPath);\n      if (seenLeaves.has(skillDirName)) {\n        throw new Error(\n          `Cannot install module '${resolved.code}': skill directories '${seenLeaves.get(skillDirName)}' and ` +\n            `'${skillPath}' share the leaf name '${skillDirName}' and would overwrite each other. ` +\n            `Skill directory names must be unique.`,\n        );\n      }\n      seenLeaves.set(skillDirName, skillPath);\n      const skillTarget = path.join(targetPath, skillDirName);\n      await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, moduleConfig);\n    }\n\n    // Place module-help.csv at the module root.\n    const helpTarget = path.join(targetPath, 'module-help.csv');\n    if (resolved.moduleHelpCsvPath) {\n      // Strategies 1-4: copy the existing file.\n      await fs.copy(resolved.moduleHelpCsvPath, helpTarget, { overwrite: true });\n      if (fileTrackingCallback) fileTrackingCallback(helpTarget);\n    } else if (resolved.synthesizedHelpCsv) {\n      // Strategy 5: write synthesized content.","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/tools/installer/modules/official-modules.js#L331-L367","documentation":"Thrown by OfficialModules._copyResolvedSkills when a resolved module's skillPaths contain two directories whose path.basename() (leaf) collide. Skills are flattened into the target directory by leaf name, so a collision would silently overwrite one skill with the other; the installer throws to fail loud. Both marketplace-plugin installs (install) and custom-source installs (installFromResolution) route through this method, so the guard covers both.","triggerScenarios":"Calling installer.install() or installer.installFromResolution() for a module whose module.yaml/manifest declares two skill paths that end in the same directory name, e.g. ['vendor/foo/skills/bar', 'vendor/baz/skills/bar']. The basename 'bar' is seen twice and the second triggers the throw.","commonSituations":"A module author reorganizes skill folders but leaves two directories with identical leaf names. A marketplace plugin aggregates skills from multiple vendors without namespacing. Two modules were merged into one without renaming nested skill dirs. Custom --custom-source repo structure changed and now produces duplicate leaves.","solutions":["Inspect the two paths printed in the error: they share the trailing directory name. Rename one of the offending skill directories in the module source so every leaf is unique.","If you don't control the source (remote marketplace/custom URL), report the duplicate-leaf packaging bug to the module maintainer and pin a known-good version via --pin or a prior commit.","Re-run install/update after the rename. Verify with: ls the resolved skillPaths and confirm no two share a basename."],"exampleFix":"// before (module.yaml declares two skills with same leaf)\n//   skills:\n//     - path: vendor/foo/skills/research\n//     - path: vendor/bar/skills/research   // collides on 'research'\n//\n// after: rename one directory in the source repo\n//   skills:\n//     - path: vendor/foo/skills/research\n//     - path: vendor/bar/skills/discovery-research","handlingStrategy":"validation","validationCode":"function assertUniqueSkillLeaves(resolved) {\n  const seen = new Map();\n  for (const p of resolved.skillPaths || []) {\n    const leaf = path.basename(p);\n    if (seen.has(leaf)) {\n      throw new Error(`Duplicate skill leaf '${leaf}': ${seen.get(leaf)} and ${p}`);\n    }\n    seen.set(leaf, p);\n  }\n}\n// call before installFromResolution / install\nassertUniqueSkillLeaves(resolved);\nawait modules.installFromResolution(resolved, bmadDir);","typeGuard":"function isResolvedModule(r) {\n  return r && typeof r === 'object'\n    && typeof r.code === 'string'\n    && Array.isArray(r.skillPaths)\n    && r.skillPaths.every((p) => typeof p === 'string');\n}","tryCatchPattern":"try {\n  await modules.installFromResolution(resolved, bmadDir, tracker, opts);\n} catch (e) {\n  if (/share the leaf name/.test(e.message)) {\n    // module packaging bug — surface the pair and abort this module\n    console.error('Module has duplicate skill leaf names; fix the source:', e.message);\n    continue;\n  }\n  throw e;\n}","preventionTips":["When authoring a module, namespace nested skill dirs (vendor/skill) and run a pre-publish check that all skillPath leaves are unique.","Add a unit test that asserts every shipped module's skillPaths have distinct basenames."],"tags":["installer","module-packaging","skill-flattening","filesystem"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}