diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts index 860280aa91e89..b4ce31cac866c 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts @@ -209,6 +209,71 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { }), ); + it.effect("resolves each install record's project repository once per discovery", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + // Real path: macOS temp dirs sit behind a /var -> /private/var symlink. + const tempDir = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" }), + ); + const configDir = path.join(tempDir, "claude-home"); + const workspace = path.join(tempDir, "workspace"); + const otherProject = path.join(tempDir, "other", "packages", "app"); + const sameRepoProject = path.join(workspace, "packages", "app"); + yield* fs.makeDirectory(path.join(workspace, ".git"), { recursive: true }); + yield* fs.makeDirectory(path.join(tempDir, "other", ".git"), { recursive: true }); + yield* fs.makeDirectory(otherProject, { recursive: true }); + yield* fs.makeDirectory(sameRepoProject, { recursive: true }); + const elsewhereInstall = path.join(configDir, "plugins", "elsewhere"); + const sameRepoInstall = path.join(configDir, "plugins", "same-repo"); + yield* writeSkill(path.join(elsewhereInstall, "skills"), "elsewhere", "# Elsewhere"); + yield* writeSkill(path.join(sameRepoInstall, "skills"), "same-repo", "# Same repo"); + const records = Array.from({ length: 4 }, () => ({ + scope: "project", + projectPath: otherProject, + installPath: elsewhereInstall, + })); + const plugins = ["a@shop", "b@shop", "c@shop"]; + yield* fs.writeFileString( + path.join(configDir, "plugins", "installed_plugins.json"), + JSON.stringify({ + plugins: { + ...Object.fromEntries(plugins.map((key) => [key, records])), + "d@shop": [ + { scope: "project", projectPath: sameRepoProject, installPath: sameRepoInstall }, + ], + }, + }), + ); + yield* fs.writeFileString( + path.join(configDir, "settings.json"), + JSON.stringify({ + enabledPlugins: Object.fromEntries([...plugins, "d@shop"].map((key) => [key, true])), + }), + ); + + const probed: Array = []; + const skills = yield* discoverClaudeSkills({ homePath: configDir }, workspace).pipe( + Effect.provideService(FileSystem.FileSystem, { + ...fs, + exists: (target) => { + probed.push(target); + return fs.exists(target); + }, + }), + ); + assert.strictEqual( + probed.filter((target) => target === path.join(otherProject, ".git")).length, + 1, + ); + assert.deepEqual( + skills.map((skill) => skill.name), + ["d:same-repo"], + ); + }), + ); + it.effect("publishes and dispatches canonical plugin folder names regardless of aliases", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.ts b/apps/server/src/provider/Drivers/ClaudeSkills.ts index c73385374fbc0..2bcf9ca472f0c 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.ts @@ -143,7 +143,8 @@ function claudeSettingsPaths( ]; } -const findRepositoryRoot = Effect.fn("findRepositoryRoot")(function* ( +// Untraced: skill discovery resolves every distinct plugin project path, too often for spans. +const findRepositoryRoot = Effect.fnUntraced(function* ( cwd: string, ): Effect.fn.Return { const fileSystem = yield* FileSystem.FileSystem; @@ -164,7 +165,7 @@ const findRepositoryRoot = Effect.fn("findRepositoryRoot")(function* ( } }); -const findPluginRepositoryRoot = Effect.fn("findPluginRepositoryRoot")(function* (cwd: string) { +const findPluginRepositoryRoot = Effect.fnUntraced(function* (cwd: string) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* findRepositoryRoot(cwd); @@ -336,6 +337,8 @@ const discoverPluginSkillRoots = Effect.fn("discoverPluginSkillRoots")(function* cwd === undefined ? undefined : yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => path.resolve(cwd))); + // Install records repeat project paths, often hundreds of times per discovery. + const projectRepositoryRoots = new Map(); const repositoryRoot = workspace === undefined ? undefined : yield* findPluginRepositoryRoot(workspace); @@ -347,11 +350,13 @@ const discoverPluginSkillRoots = Effect.fn("discoverPluginSkillRoots")(function* Effect.gen(function* () { if (record.scope === "user" || record.scope === "managed") return true; if (record.projectPath === undefined || workspace === undefined) return false; - if (path.resolve(record.projectPath) === workspace) return true; - return ( - repositoryRoot !== undefined && - (yield* findPluginRepositoryRoot(record.projectPath)) === repositoryRoot - ); + const projectPath = path.resolve(record.projectPath); + if (projectPath === workspace) return true; + if (repositoryRoot === undefined) return false; + if (!projectRepositoryRoots.has(projectPath)) { + projectRepositoryRoots.set(projectPath, yield* findPluginRepositoryRoot(projectPath)); + } + return projectRepositoryRoots.get(projectPath) === repositoryRoot; }), ); // The native loader keeps registry order, preferring the first install that exists.