Просмотр исходного кода

fix(core): prefer nearest skill source (#41788)

Kit Langton 5 дней назад
Родитель
Сommit
2b6dd2559c

+ 2 - 2
packages/core/src/config.ts

@@ -204,13 +204,13 @@ export const layer = (options?: Options) =>
         const claude = [
           ...new Set([
             ...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []),
-            ...discovered.filter((item) => path.basename(item) === ".claude"),
+            ...discovered.filter((item) => path.basename(item) === ".claude").toReversed(),
           ]),
         ].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) }))
         const agents = [
           ...new Set([
             ...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []),
-            ...discovered.filter((item) => path.basename(item) === ".agents"),
+            ...discovered.filter((item) => path.basename(item) === ".agents").toReversed(),
           ]),
         ].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) }))
 

+ 4 - 4
packages/core/test/config/config.test.ts

@@ -1464,13 +1464,13 @@ describe("Config", () => {
             ])
             expect(entries.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([
               AbsolutePath.make(globalAgents),
-              AbsolutePath.make(path.join(directory, ".agents")),
               AbsolutePath.make(path.join(root, ".agents")),
+              AbsolutePath.make(path.join(directory, ".agents")),
             ])
             expect(entries.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([
               AbsolutePath.make(globalClaude),
-              AbsolutePath.make(path.join(directory, ".claude")),
               AbsolutePath.make(path.join(root, ".claude")),
+              AbsolutePath.make(path.join(directory, ".claude")),
             ])
             expect(documents.map((document) => document.info.$schema)).toEqual([
               "global",
@@ -1483,11 +1483,11 @@ describe("Config", () => {
             ])
             expect(entries.map((entry) => (entry.type === "document" ? entry.info.$schema : entry.path))).toEqual([
               AbsolutePath.make(globalClaude),
-              AbsolutePath.make(path.join(directory, ".claude")),
               AbsolutePath.make(path.join(root, ".claude")),
+              AbsolutePath.make(path.join(directory, ".claude")),
               AbsolutePath.make(globalAgents),
-              AbsolutePath.make(path.join(directory, ".agents")),
               AbsolutePath.make(path.join(root, ".agents")),
+              AbsolutePath.make(path.join(directory, ".agents")),
               "global",
               AbsolutePath.make(global),
               "outside",

+ 53 - 0
packages/core/test/config/skill.test.ts

@@ -16,6 +16,7 @@ import { SkillFile } from "@opencode-ai/core/config/plugin/skill-file"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { Watcher } from "@opencode-ai/core/filesystem/watcher"
 import { Bus } from "@opencode-ai/core/bus"
+import { Credential } from "@opencode-ai/core/credential"
 import { FSUtil } from "@opencode-ai/util/fs-util"
 import { Global } from "@opencode-ai/util/global"
 import { LayerNode } from "@opencode-ai/util/effect/layer-node"
@@ -23,6 +24,8 @@ import { Location } from "@opencode-ai/core/location"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { Skill } from "@opencode-ai/core/skill"
 import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
+import { WellKnown } from "@opencode-ai/core/wellknown"
+import { emptyCredentialNode, emptyWellknownNode } from "../fixture/config-nodes"
 import { tmpdir } from "../fixture/tmpdir"
 import { location } from "../fixture/location"
 import { testEffect } from "../lib/effect"
@@ -91,6 +94,25 @@ const start = (skills: string[], directory: string) =>
     directory,
   )
 
+const discover = (directory: string, global: string) =>
+  Effect.gen(function* () {
+    const config = yield* Config.Service
+    return yield* config.entries()
+  }).pipe(
+    Effect.provide(
+      AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
+        [
+          Location.node,
+          Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
+        ],
+        [Global.node, Global.layerWith({ config: global, home: path.join(global, "home") })],
+        [Credential.node, emptyCredentialNode],
+        [WellKnown.node, emptyWellknownNode],
+        [Watcher.node, Watcher.testLayer],
+      ]),
+    ),
+  )
+
 function emitAndWait(update: Watcher.Update) {
   return Effect.gen(function* () {
     const watcher = yield* Watcher.Test
@@ -218,6 +240,37 @@ describe("ConfigSkillPlugin.Plugin", () => {
     ),
   )
 
+  it.live("prefers a worktree skill over the parent checkout copy", () =>
+    Effect.acquireRelease(
+      Effect.promise(() => tmpdir()),
+      (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+    ).pipe(
+      Effect.flatMap((tmp) =>
+        Effect.gen(function* () {
+          const checkout = path.join(tmp.path, "repo")
+          const worktree = path.join(checkout, ".worktrees", "feature")
+          const parentSkills = path.join(checkout, ".agents", "skills")
+          const worktreeSkills = path.join(worktree, ".agents", "skills")
+          yield* Effect.promise(async () => {
+            await fs.mkdir(path.join(checkout, ".git"), { recursive: true })
+            await fs.mkdir(path.join(parentSkills, "review"), { recursive: true })
+            await fs.mkdir(path.join(worktreeSkills, "review"), { recursive: true })
+            await fs.writeFile(path.join(worktree, ".git"), "gitdir: ../../../.git/worktrees/feature\n")
+            await write(parentSkills, "review", "Parent checkout")
+            await write(worktreeSkills, "review", "Worktree")
+          })
+
+          const entries = yield* discover(worktree, path.join(tmp.path, "global"))
+          const skill = yield* startEntries(entries, worktree)
+          const review = (yield* skill.list()).find((item) => item.id === "review")
+
+          expect(review?.description).toBe("Worktree")
+          expect(review?.location).toBe(AbsolutePath.make(path.join(worktreeSkills, "review", "SKILL.md")))
+        }),
+      ),
+    ),
+  )
+
   it.live("keeps directory skills when a URL source fails", () =>
     Effect.acquireRelease(
       Effect.promise(() => tmpdir()),