fix(skills): 兼容 Codex 最小元数据校验

This commit is contained in:
Mengz 2026-07-27 11:19:26 +08:00
parent 335a991e15
commit 5b40a90887
3 changed files with 70 additions and 9 deletions

View File

@ -0,0 +1,23 @@
# Skill 元数据双模式校验
维护者效率 Skill 使用 Codex/Agent Skill 的最小 frontmatter只要求 `name`
`description`。仓库原有校验器则无条件要求 `version`、`metadata.requires.bins`
`metadata.cliHelp`,导致符合 Codex 规范的 Skill 在仓库测试中被误报。
本次把校验调整为双模式:
- 只声明 `name``description` 时,按 Codex 最小规范校验。
- 一旦声明 `version`、`requires.bins` 或 `cliHelp` 中任一执行元数据,就继续要求
三项完整,并校验语义版本、`gitlink-cli` 依赖和命令帮助入口。
- Skill 名称、目录一致性和描述长度规则保持不变。
这样不会为了兼容 Codex 而放过不完整的旧版执行元数据,同时确保五个维护专项 Skill
`gitlink-maintenance-orchestrator` 能通过仓库校验与 Codex `quick_validate.py`
验证命令:
```bash
go test ./internal/skillmeta \
-run 'TestValidateCatchesBadSkills|TestValidateAcceptsCodexMinimalFrontmatter' \
-count=1
```

View File

@ -80,17 +80,25 @@ func validateSkill(root, name string) []Problem {
case fm.Name != name:
add("name", fmt.Sprintf("must equal the directory name %q", name))
}
if !semverRe.MatchString(fm.Version) {
add("version", "must be semantic version X.Y.Z")
}
if utf8.RuneCountInString(fm.Description) < minDescriptionRunes {
add("description", fmt.Sprintf("must be at least %d characters; it is the router's only routing signal", minDescriptionRunes))
}
if !containsString(fm.Metadata.Requires.Bins, "gitlink-cli") {
add("metadata.requires.bins", `must contain "gitlink-cli"`)
}
if strings.TrimSpace(fm.Metadata.CLIHelp) == "" {
add("metadata.cliHelp", "must name the command group, e.g. \"gitlink-cli x --help\"")
// Codex-compatible skills only require name and description. Once a skill
// opts into GitLink's legacy execution metadata, validate that block as a
// complete unit instead of accepting a partially configured declaration.
hasExecutionMetadata := fm.Version != "" ||
len(fm.Metadata.Requires.Bins) > 0 ||
strings.TrimSpace(fm.Metadata.CLIHelp) != ""
if hasExecutionMetadata {
if !semverRe.MatchString(fm.Version) {
add("version", "must be semantic version X.Y.Z")
}
if !containsString(fm.Metadata.Requires.Bins, "gitlink-cli") {
add("metadata.requires.bins", `must contain "gitlink-cli"`)
}
if strings.TrimSpace(fm.Metadata.CLIHelp) == "" {
add("metadata.cliHelp", "must name the command group, e.g. \"gitlink-cli x --help\"")
}
}
return ps
}

View File

@ -1,6 +1,10 @@
package skillmeta
import "testing"
import (
"os"
"path/filepath"
"testing"
)
// TestRepoSkillsValid treats the real skills/ registry as a regression
// baseline: once fixed, every SKILL.md must keep passing the schema.
@ -37,3 +41,29 @@ func TestValidateCatchesBadSkills(t *testing.T) {
}
}
}
func TestValidateAcceptsCodexMinimalFrontmatter(t *testing.T) {
root := t.TempDir()
skillDir := filepath.Join(root, "gitlink-codex-minimal")
if err := os.Mkdir(skillDir, 0o755); err != nil {
t.Fatalf("create skill directory: %v", err)
}
src := []byte(`---
name: gitlink-codex-minimal
description: "A Codex-compatible skill with only the required routing metadata."
---
# Test skill
`)
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), src, 0o644); err != nil {
t.Fatalf("write SKILL.md: %v", err)
}
problems, err := Validate(root)
if err != nil {
t.Fatalf("validate testdata: %v", err)
}
if len(problems) != 0 {
t.Fatalf("Codex-compatible minimal frontmatter must pass validation: %v", problems)
}
}