authorgravatar for snorteman@gmail.comScott Norteman <snorteman@gmail.com> 2026-10-11 02:46:54-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 01:46:54-05:00
log6fa0dd8ccabfee961ba01ea92267eded7d07cc51
treee7cf4418b890a00ae8a95ba2a8b3896037340d37
parentbf725cb80d79334601c2833cb599524a3669e03b
signature Signed by PGP key B5690EEEBB952194

fix(provider-cursor): a skill tree deeper than the scan limit no longer hides every skill (#17389)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 86 insertions(+), 4 deletions(-)

packages/provider-cursor/src/server/skills.test.ts+83
...@@ -102,6 +102,89 @@ describe("Cursor skills", () => {...@@ -102,6 +102,89 @@ describe("Cursor skills", () => {
102 }),102 }),
103 ));103 ));
104104
105 it("skips a skill tree deeper than the limit instead of failing discovery", async () =>
106 await runNode(
107 Effect.gen(function* () {
108 const fileSystem = yield* FileSystem.FileSystem;
109 const path = yield* Path.Path;
110 const tempDirectory = (prefix: string) =>
111 fileSystem
112 .makeTempDirectoryScoped({ directory: NodeOS.tmpdir(), prefix })
113 .pipe(Effect.flatMap((directory) => fileSystem.realPath(directory)));
114 const userHome = yield* tempDirectory("cursor-skills-home-");
115 const workspace = yield* tempDirectory("cursor-skills-workspace-");
116 const writeSkill = Effect.fn("writeCursorSkill")(function* (directory: string) {
117 yield* fileSystem.makeDirectory(directory, { recursive: true });
118 yield* fileSystem.writeFileString(path.join(directory, "SKILL.md"), "---\n---\n");
119 });
120 const userRoot = path.join(userHome, ".claude", "skills");
121 // The root is depth 0, so `deep-skill` sits at the depth limit and `below` past it.
122 const atLimit = path.join(
123 userRoot,
124 "deep",
125 "2",
126 "3",
127 "4",
128 "5",
129 "6",
130 "7",
131 "8",
132 "9",
133 "deep-skill",
134 );
135
136 yield* writeSkill(path.join(workspace, ".agents", "skills", "review"));
137 yield* writeSkill(atLimit);
138 yield* writeSkill(path.join(atLimit, "below"));
139 yield* writeSkill(path.join(userRoot, "later"));
140
141 const expected = [
142 {
143 name: "deep-skill",
144 path: path.join(atLimit, "SKILL.md"),
145 scope: "user",
146 enabled: true,
147 },
148 {
149 name: "later",
150 path: path.join(userRoot, "later", "SKILL.md"),
151 scope: "user",
152 enabled: true,
153 },
154 {
155 name: "review",
156 path: path.join(workspace, ".agents", "skills", "review", "SKILL.md"),
157 scope: "project",
158 enabled: true,
159 },
160 ];
161 expect(yield* discoverCursorSkills(workspace, { HOME: userHome })).toEqual(expected);
162 expect(yield* probeCursorSkills(workspace, { HOME: userHome })).toEqual(expected);
163 }),
164 ));
165
166 it("still fails discovery when a root holds more entries than the scan allows", async () =>
167 await runNode(
168 Effect.gen(function* () {
169 const fileSystem = yield* FileSystem.FileSystem;
170 const path = yield* Path.Path;
171 const userHome = yield* fileSystem.makeTempDirectoryScoped({
172 directory: NodeOS.tmpdir(),
173 prefix: "cursor-skills-home-",
174 });
175 const root = path.join(userHome, ".cursor", "skills");
176 yield* fileSystem.makeDirectory(root, { recursive: true });
177 yield* Effect.forEach(
178 Array.from({ length: 10_001 }, (_, index) => path.join(root, `entry-${index}`)),
179 (file) => fileSystem.writeFileString(file, ""),
180 { concurrency: 64, discard: true },
181 );
182
183 const error = yield* Effect.flip(probeCursorSkills(undefined, { HOME: userHome }));
184 expect(error.message).toContain("scan-budget-exhausted");
185 }),
186 ));
187
105 it.skipIf(!symlinksSupported)(188 it.skipIf(!symlinksSupported)(
106 "treats a symlinked skill outside the root as a package boundary",189 "treats a symlinked skill outside the root as a package boundary",
107 async () =>190 async () =>
packages/provider-cursor/src/server/skills.ts+3-4
...@@ -204,10 +204,9 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (...@@ -204,10 +204,9 @@ const discoverSkillsInRoot = Effect.fn("discoverCursorSkillsInRoot")(function* (
204 const child = path.join(directory, entry);204 const child = path.join(directory, entry);
205 const info = yield* orUndefined(fileSystem.stat(child), input.budget);205 const info = yield* orUndefined(fileSystem.stat(child), input.budget);
206 if (info?.type !== "Directory") continue;206 if (info?.type !== "Directory") continue;
207 if (depth >= MAX_SKILL_DEPTH) {207 // A tree deeper than the limit is skipped rather than fatal: the same tree would fail
208 input.budget.exhausted = true;208 // every refresh and hide every other skill with it.
209 return;209 if (depth >= MAX_SKILL_DEPTH) continue;
210 }
211 yield* visit(child, depth + 1);210 yield* visit(child, depth + 1);
212 }211 }
213 });212 });