diff --git a/doc/changes/skill-validator-codex-compat.md b/doc/changes/skill-validator-codex-compat.md new file mode 100644 index 0000000..24c051a --- /dev/null +++ b/doc/changes/skill-validator-codex-compat.md @@ -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 +``` diff --git a/internal/skillmeta/validate.go b/internal/skillmeta/validate.go index 06ed19a..9e1c603 100644 --- a/internal/skillmeta/validate.go +++ b/internal/skillmeta/validate.go @@ -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 } diff --git a/internal/skillmeta/validate_test.go b/internal/skillmeta/validate_test.go index c35ee84..2e807c6 100644 --- a/internal/skillmeta/validate_test.go +++ b/internal/skillmeta/validate_test.go @@ -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) + } +}