authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-04 08:23:15-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-04 08:23:15-03:00
logb3b6ae2cebbae39a706038911dbb8c85c7120be7
treeeda1853a62e49a08e5bb0e5ec415807504f0f1e3
parente01e58916fa51ec52389cabba1b5fbba6344723d
signature Signed by PGP key B5690EEEBB952194

fix(server): recover claude skill scalar frontmatter (#15452)


2 files changed, 72 insertions(+), 3 deletions(-)

apps/server/src/provider/Drivers/ClaudeSkills.test.ts+53-1
......@@ -154,6 +154,48 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
154154 }),
155155 );
156156
157 it.effect("recovers colon-bearing descriptions with invocation metadata intact", () =>
158 Effect.gen(function* () {
159 const fs = yield* FileSystem.FileSystem;
160 const path = yield* Path.Path;
161 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" });
162 const configDir = path.join(tempDir, "claude-home");
163
164 for (const [description, comment] of [
165 ["Browser automation + AI test authoring via kane-cli: run browser objectives, ...", ""],
166 ['Read C:\\skills\\guide#tag: continue with "quoted".', " # trailing: comment"],
167 ] as const) {
168 yield* writeSkill(
169 path.join(configDir, "skills"),
170 "kane-cli",
171 [
172 "---",
173 "name: frontmatter-alias",
174 `description: ${description}${comment}`,
175 "allowed-tools: [Read, Write]",
176 "disable-model-invocation: yes",
177 "user-invocable: no",
178 "---",
179 ].join("\n"),
180 );
181
182 const skills = yield* discoverClaudeSkills({ homePath: configDir }, undefined);
183
184 assert.deepEqual(skills, [
185 {
186 name: "kane-cli",
187 path: path.join(configDir, "skills", "kane-cli", "SKILL.md"),
188 enabled: true,
189 scope: "user",
190 description,
191 userInvocationOnly: true,
192 userInvocable: false,
193 },
194 ]);
195 }
196 }),
197 );
198
157199 it.effect("falls back to the directory name and skips malformed frontmatter", () =>
158200 Effect.gen(function* () {
159201 const fs = yield* FileSystem.FileSystem;
......@@ -163,7 +205,17 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
163205 const skillsDir = path.join(configDir, "skills");
164206
165207 yield* writeSkill(skillsDir, "no-frontmatter", "# Just a heading\n");
166 yield* writeSkill(skillsDir, "broken-yaml", "---\nname: [unclosed\n---\n");
208 for (const [directoryName, field] of [
209 ["broken-yaml", "name: [unclosed"],
210 ["broken-tools", "allowed-tools: [Read, Write"],
211 ["broken-quoted", 'name: "unclosed: text'],
212 ] as const) {
213 yield* writeSkill(
214 skillsDir,
215 directoryName,
216 ["---", "description: Run: browser objectives.", field, "---"].join("\n"),
217 );
218 }
167219 // A stray file (not a directory with SKILL.md) must be skipped.
168220 yield* fs.makeDirectory(skillsDir, { recursive: true });
169221 yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill");
apps/server/src/provider/Drivers/ClaudeSkills.ts+19-2
......@@ -75,11 +75,28 @@ function parseSkillFrontmatter(contents: string): SkillFrontmatter {
7575 return { kind: "missing" };
7676 }
7777
78 const frontmatter = match[1] ?? "";
7879 let parsed: unknown;
7980 try {
80 parsed = parseYamlDocument(match[1] ?? "");
81 parsed = parseYamlDocument(frontmatter);
8182 } catch {
82 return { kind: "malformed" };
83 // Claude Code accepts plain scalars containing `: `. Repair only those,
84 // leaving comments and YAML structure for the full-document parser.
85 const repaired = frontmatter.replace(
86 /^([\w-]+:[ \t]*)([^\r\n]*)/gm,
87 (line, prefix: string, value: string) => {
88 const scalar = value.split(/[ \t]+#/)[0] ?? "";
89 if (!/:[ \t]/.test(scalar) || /^(?:["'[\]{}|>&*!#%@`]|[-?:](?:[ \t]|$))/.test(scalar)) {
90 return line;
91 }
92 return `${prefix}${JSON.stringify(scalar)}${value.slice(scalar.length)}`;
93 },
94 );
95 try {
96 parsed = parseYamlDocument(repaired);
97 } catch {
98 return { kind: "malformed" };
99 }
83100 }
84101 if (typeof parsed !== "object" || parsed === null) {
85102 return { kind: "malformed" };