[Bug]: SKILL.md frontmatter 手写解析器不支持 YAML 块标量与 allowed-tools 字段,编辑后静默丢字段
Area
Skill Store / Skills
Platform
Multiple platforms(桌面端 + CLI,解析逻辑在两侧各自复制了一份)
Version
0.5.9-beta.1(main 分支当前实现,未见相关修复记录)
What happened?
SKILL.md 的 YAML frontmatter 解析没有使用标准 YAML 解析库,而是两处几乎相同的
手写逐行解析器:
apps/desktop/src/main/services/skill-validator.ts 的 parseSkillMd()
packages/core/src/cli/skill-cli-service.ts 的 parseSkillMd()
这两处实现按“找每行第一个 : 做 key/value 切分”处理 frontmatter,不识别 YAML
块标量语法(|、|-、>、>-),也没有在 SkillFrontmatter/ParsedSkillMd
接口和序列化函数 exportAsSkillMd() 中支持官方标准字段 allowed-tools。
实测复现(对当前 skill-validator.ts 的 parseSkillMd() 逻辑原样抽取执行):
输入 SKILL.md:
---
name: pdf-processing
description: |-
Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs.
Use when working with PDF documents or when the user mentions PDFs, forms, or document extraction.
license: Apache-2.0
allowed-tools: Read, Bash
metadata:
author: your-org
version: "1.2"
---
# PDF Processing
Body content here.
解析输出(JSON.stringify(parseSkillMd(sample))):
{
"frontmatter": {
"name": "pdf-processing",
"description": "|-",
"license": "Apache-2.0",
"metadata": {
"author": "your-org",
"version": "\"1.2\""
}
},
"body": "# PDF Processing\n\nBody content here.",
"raw": "... (原始内容保留) ..."
}
问题点:
description 多行块标量(|-)内容整体丢失,frontmatter.description
变成了字面字符串 "|-",原本两行真实描述内容全部消失。description
是 Skill 是否被模型自动触发的关键字段,丢失后等价于该 Skill 描述为空/错误。
allowed-tools 字段完全没有出现在解析结果里。SkillFrontmatter
接口本身没有声明这个字段,逐行循环里也没有任何分支处理它,导致这一行被
静默丢弃,没有任何报错或警告。
- (次要,随手复现时发现)
metadata 嵌套字段的引号未被剥除,
metadata.version 结果是 "\"1.2\""(多余的转义引号残留),说明嵌套 map
的取值逻辑复用了顶层逻辑但边界不完整。
更严重的是回写路径。apps/desktop/src/main/services/skill-repo-sync.ts 的
syncFrontmatterToRepo() 会在用户编辑 Skill 元数据(如 description、tags、
author 等触发 METADATA_KEYS 命中)后,调用 SkillInstaller.exportAsSkillMd()
重写整份 SKILL.md 文件:
const newContent = SkillInstaller.exportAsSkillMd({
name: updatedSkill.name,
description: updatedSkill.description ?? undefined,
version: updatedSkill.version ?? undefined,
author: updatedSkill.author ?? undefined,
tags: updatedSkill.tags ?? [],
instructions: body,
compatibility: parsed?.frontmatter.compatibility ?? undefined,
license: parsed?.frontmatter.license ?? undefined,
});
exportAsSkillMd()(apps/desktop/src/main/services/skill-installer-export.ts
与 packages/core/src/cli/skill-cli-service.ts 中各有一份)的参数签名里同样
没有 allowedTools 或 metadata。因此:
- 只要用户在 UI 里编辑一次任意元数据字段(哪怕只是改个 tag),触发
syncFrontmatterToRepo() 重写 SKILL.md,原文件中的 allowed-tools 就会在
这次重写中被整体丢弃,无法恢复(除非该 Skill 有版本历史快照)。
- 如果原始
description 是用 |-/> 写的多行块标量,编辑其他字段触发的
重写会把它替换成 exportAsSkillMd() 重新生成的单行字符串,内容与用户原始
撰写的多行描述不一致(取决于解析阶段是否已经丢失,通常是丢失后写回一个
几乎空的值)。
What did you expect?
description 无论写成单行、加引号,还是 YAML 块标量(|/|-/>/>-)
多行形式,解析后都应该得到与该字段语义一致的完整字符串内容,不丢失、不
截断。
allowed-tools 作为 Agent Skills 官方规范定义的标准可选字段(与
name/description/license/compatibility/metadata 同属六个规范
字段),应该被正确解析、在内存模型中保留,并在 exportAsSkillMd()
重写文件时原样写回,不因为编辑其他元数据字段而被静默清空。
- 理想情况下 frontmatter 解析/序列化应改用标准 YAML 库(例如
js-yaml),
替代当前两处重复的手写逐行解析器,从根本上避免块标量、嵌套 map、flow
数组等 YAML 语法子集缺失的问题;同时应统一到 packages/core,消除
apps/desktop 与 packages/core 中两份定义不完全一致的
SkillFrontmatter/ParsedSkillMd/exportAsSkillMd。
Steps to reproduce
- 准备一个 SKILL.md,其中
description 使用 YAML 块标量语法(description: |-
后接多行缩进文本),并加入 allowed-tools: Read, Bash 字段(可参考上文
"What happened?" 中的示例内容)。
- 通过“扫描本地 / Git 仓库导入 / My Skills 手动添加”等任意方式将该 Skill
纳入 PromptHub(走 parseSkillMd() 解析路径)。
- 查看该 Skill 在 UI 中的 description 展示,或直接检查 DB 中
skills.description 字段的值:应会发现描述内容为空、或仅为字面字符串
|-,而不是原始撰写的多行描述。
- 检查解析后的内存对象/元数据模型(或直接在代码中断点
parseSkillMd()
返回值):allowed-tools 字段不存在于结果中。
- 在 UI 中编辑该 Skill 的任意其它元数据字段(例如新增一个 tag)并保存,
触发 syncFrontmatterToRepo() 重写。
- 重新打开磁盘上的 SKILL.md 文件:
allowed-tools: Read, Bash 这一行已经
从文件中消失。
Logs, screenshots, or extra context
- 涉及文件:
apps/desktop/src/main/services/skill-validator.ts(parseSkillMd,
SkillFrontmatter/ParsedSkillMd 接口定义)
packages/core/src/cli/skill-cli-service.ts(重复实现的
parseSkillMd/exportAsSkillMd/ParsedSkillMd)
apps/desktop/src/main/services/skill-installer-export.ts
(exportAsSkillMd,序列化时未转发 allowed-tools/metadata)
apps/desktop/src/main/services/skill-repo-sync.ts
(syncFrontmatterToRepo、METADATA_KEYS,触发静默重写丢字段的路径)
- 官方规范参考:Agent Skills open standard 定义 6 个字段:
name(必需)、description(必需)、license、compatibility、
metadata、allowed-tools(均为可选)。规范允许 description 使用标准
YAML scalar 语法(包含块标量),allowed-tools 支持逗号分隔字符串(如
Read, Bash)及带参数的工具限定(如 Bash(git add *))。当前实现只覆盖
裸单行标量这一种最简子集。
- 本地复现脚本:直接从
skill-validator.ts 的 parseSkillMd() 抽取原始逻辑
在 Node 中运行得到上文 JSON 输出(详见 "What happened?" 章节,未修改任何
项目源码)。
- 关联分析:
spec/changes/active/(如落地修复,建议开一个 change,例如
skill-frontmatter-yaml-parser,覆盖:是否引入 js-yaml 依赖、合并
apps/desktop/packages/core 两处重复实现到 packages/core、补齐
allowed-tools/metadata 全链路(解析 -> DB/内存模型 -> 序列化)、
迁移期已存在 Skill 的兼容性与回归测试)。
Before submitting
[Bug]: SKILL.md frontmatter 手写解析器不支持 YAML 块标量与
allowed-tools字段,编辑后静默丢字段Area
Skill Store / Skills
Platform
Multiple platforms(桌面端 + CLI,解析逻辑在两侧各自复制了一份)
Version
0.5.9-beta.1(main 分支当前实现,未见相关修复记录)
What happened?
SKILL.md的 YAML frontmatter 解析没有使用标准 YAML 解析库,而是两处几乎相同的手写逐行解析器:
apps/desktop/src/main/services/skill-validator.ts的parseSkillMd()packages/core/src/cli/skill-cli-service.ts的parseSkillMd()这两处实现按“找每行第一个
:做 key/value 切分”处理 frontmatter,不识别 YAML块标量语法(
|、|-、>、>-),也没有在SkillFrontmatter/ParsedSkillMd接口和序列化函数
exportAsSkillMd()中支持官方标准字段allowed-tools。实测复现(对当前
skill-validator.ts的parseSkillMd()逻辑原样抽取执行):输入 SKILL.md:
解析输出(
JSON.stringify(parseSkillMd(sample))):{ "frontmatter": { "name": "pdf-processing", "description": "|-", "license": "Apache-2.0", "metadata": { "author": "your-org", "version": "\"1.2\"" } }, "body": "# PDF Processing\n\nBody content here.", "raw": "... (原始内容保留) ..." }问题点:
description多行块标量(|-)内容整体丢失,frontmatter.description变成了字面字符串
"|-",原本两行真实描述内容全部消失。description是 Skill 是否被模型自动触发的关键字段,丢失后等价于该 Skill 描述为空/错误。
allowed-tools字段完全没有出现在解析结果里。SkillFrontmatter接口本身没有声明这个字段,逐行循环里也没有任何分支处理它,导致这一行被
静默丢弃,没有任何报错或警告。
metadata嵌套字段的引号未被剥除,metadata.version结果是"\"1.2\""(多余的转义引号残留),说明嵌套 map的取值逻辑复用了顶层逻辑但边界不完整。
更严重的是回写路径。
apps/desktop/src/main/services/skill-repo-sync.ts的syncFrontmatterToRepo()会在用户编辑 Skill 元数据(如 description、tags、author 等触发
METADATA_KEYS命中)后,调用SkillInstaller.exportAsSkillMd()重写整份 SKILL.md 文件:
exportAsSkillMd()(apps/desktop/src/main/services/skill-installer-export.ts与
packages/core/src/cli/skill-cli-service.ts中各有一份)的参数签名里同样没有
allowedTools或metadata。因此:syncFrontmatterToRepo()重写 SKILL.md,原文件中的allowed-tools就会在这次重写中被整体丢弃,无法恢复(除非该 Skill 有版本历史快照)。
description是用|-/>写的多行块标量,编辑其他字段触发的重写会把它替换成
exportAsSkillMd()重新生成的单行字符串,内容与用户原始撰写的多行描述不一致(取决于解析阶段是否已经丢失,通常是丢失后写回一个
几乎空的值)。
What did you expect?
description无论写成单行、加引号,还是 YAML 块标量(|/|-/>/>-)多行形式,解析后都应该得到与该字段语义一致的完整字符串内容,不丢失、不
截断。
allowed-tools作为 Agent Skills 官方规范定义的标准可选字段(与name/description/license/compatibility/metadata同属六个规范字段),应该被正确解析、在内存模型中保留,并在
exportAsSkillMd()重写文件时原样写回,不因为编辑其他元数据字段而被静默清空。
js-yaml),替代当前两处重复的手写逐行解析器,从根本上避免块标量、嵌套 map、flow
数组等 YAML 语法子集缺失的问题;同时应统一到
packages/core,消除apps/desktop与packages/core中两份定义不完全一致的SkillFrontmatter/ParsedSkillMd/exportAsSkillMd。Steps to reproduce
description使用 YAML 块标量语法(description: |-后接多行缩进文本),并加入
allowed-tools: Read, Bash字段(可参考上文"What happened?" 中的示例内容)。
纳入 PromptHub(走
parseSkillMd()解析路径)。skills.description字段的值:应会发现描述内容为空、或仅为字面字符串|-,而不是原始撰写的多行描述。parseSkillMd()返回值):
allowed-tools字段不存在于结果中。触发
syncFrontmatterToRepo()重写。allowed-tools: Read, Bash这一行已经从文件中消失。
Logs, screenshots, or extra context
apps/desktop/src/main/services/skill-validator.ts(parseSkillMd,SkillFrontmatter/ParsedSkillMd接口定义)packages/core/src/cli/skill-cli-service.ts(重复实现的parseSkillMd/exportAsSkillMd/ParsedSkillMd)apps/desktop/src/main/services/skill-installer-export.ts(
exportAsSkillMd,序列化时未转发allowed-tools/metadata)apps/desktop/src/main/services/skill-repo-sync.ts(
syncFrontmatterToRepo、METADATA_KEYS,触发静默重写丢字段的路径)name(必需)、description(必需)、license、compatibility、metadata、allowed-tools(均为可选)。规范允许description使用标准YAML scalar 语法(包含块标量),
allowed-tools支持逗号分隔字符串(如Read, Bash)及带参数的工具限定(如Bash(git add *))。当前实现只覆盖裸单行标量这一种最简子集。
skill-validator.ts的parseSkillMd()抽取原始逻辑在 Node 中运行得到上文 JSON 输出(详见 "What happened?" 章节,未修改任何
项目源码)。
spec/changes/active/(如落地修复,建议开一个 change,例如skill-frontmatter-yaml-parser,覆盖:是否引入js-yaml依赖、合并apps/desktop/packages/core两处重复实现到packages/core、补齐allowed-tools/metadata全链路(解析 -> DB/内存模型 -> 序列化)、迁移期已存在 Skill 的兼容性与回归测试)。
Before submitting
(main 分支当前实现,复现逻辑对照最新源码抽取)。