Delete skillpackage directory

This commit is contained in:
公明
2026-04-19 19:17:32 +08:00
committed by GitHub
parent 0a5e0dc1d0
commit 62bf0f13e1
7 changed files with 0 additions and 869 deletions
-165
View File
@@ -1,165 +0,0 @@
package skillpackage
import (
"fmt"
"regexp"
"strings"
)
var reH2 = regexp.MustCompile(`(?m)^##\s+(.+)$`)
const summaryContentRunes = 6000
type markdownSection struct {
Heading string
Title string
Content string
}
func splitMarkdownSections(body string) []markdownSection {
body = strings.TrimSpace(body)
if body == "" {
return nil
}
idxs := reH2.FindAllStringIndex(body, -1)
titles := reH2.FindAllStringSubmatch(body, -1)
if len(idxs) == 0 {
return []markdownSection{{
Heading: "",
Title: "_body",
Content: body,
}}
}
var out []markdownSection
for i := range idxs {
title := strings.TrimSpace(titles[i][1])
start := idxs[i][0]
end := len(body)
if i+1 < len(idxs) {
end = idxs[i+1][0]
}
chunk := strings.TrimSpace(body[start:end])
out = append(out, markdownSection{
Heading: "## " + title,
Title: title,
Content: chunk,
})
}
return out
}
func deriveSections(body string) []SkillSection {
md := splitMarkdownSections(body)
out := make([]SkillSection, 0, len(md))
for _, ms := range md {
if ms.Title == "_body" {
continue
}
out = append(out, SkillSection{
ID: slugifySectionID(ms.Title),
Title: ms.Title,
Heading: ms.Heading,
Level: 2,
})
}
return out
}
func slugifySectionID(title string) string {
title = strings.TrimSpace(strings.ToLower(title))
if title == "" {
return "section"
}
var b strings.Builder
for _, r := range title {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
b.WriteRune(r)
case r == ' ', r == '-', r == '_':
b.WriteRune('-')
}
}
s := strings.Trim(b.String(), "-")
if s == "" {
return "section"
}
return s
}
func findSectionContent(sections []markdownSection, sec string) string {
sec = strings.TrimSpace(sec)
if sec == "" {
return ""
}
want := strings.ToLower(sec)
for _, s := range sections {
if strings.EqualFold(slugifySectionID(s.Title), want) || strings.EqualFold(s.Title, sec) {
return s.Content
}
if strings.EqualFold(strings.ReplaceAll(s.Title, " ", "-"), want) {
return s.Content
}
}
return ""
}
func buildSummaryMarkdown(name, description string, tags []string, scripts []SkillScriptInfo, sections []SkillSection, body string) string {
var b strings.Builder
if description != "" {
b.WriteString(description)
b.WriteString("\n\n")
}
if len(tags) > 0 {
b.WriteString("**Tags**: ")
b.WriteString(strings.Join(tags, ", "))
b.WriteString("\n\n")
}
if len(scripts) > 0 {
b.WriteString("### Bundled scripts\n\n")
for _, sc := range scripts {
line := "- `" + sc.RelPath + "`"
if sc.Description != "" {
line += " — " + sc.Description
}
b.WriteString(line)
b.WriteString("\n")
}
b.WriteString("\n")
}
if len(sections) > 0 {
b.WriteString("### Sections\n\n")
for _, sec := range sections {
line := "- **" + sec.ID + "**"
if sec.Title != "" && sec.Title != sec.ID {
line += ": " + sec.Title
}
b.WriteString(line)
b.WriteString("\n")
}
b.WriteString("\n")
}
mdSecs := splitMarkdownSections(body)
preview := body
if len(mdSecs) > 0 && mdSecs[0].Title != "_body" {
preview = mdSecs[0].Content
}
b.WriteString("### Preview (SKILL.md)\n\n")
b.WriteString(truncateRunes(strings.TrimSpace(preview), summaryContentRunes))
b.WriteString("\n\n---\n\n_(Summary for admin UI. Agents use Eino `skill` tool for full SKILL.md progressive loading.)_")
if name != "" {
b.WriteString(fmt.Sprintf("\n\n_Skill name: %s_", name))
}
return b.String()
}
func truncateRunes(s string, max int) string {
if max <= 0 || s == "" {
return s
}
r := []rune(s)
if len(r) <= max {
return s
}
return string(r[:max]) + "…"
}
-114
View File
@@ -1,114 +0,0 @@
package skillpackage
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
// ExtractSkillMDFrontMatterYAML returns the YAML source inside the first --- ... --- block and the markdown body.
func ExtractSkillMDFrontMatterYAML(raw []byte) (fmYAML string, body string, err error) {
text := strings.TrimPrefix(string(raw), "\ufeff")
if strings.TrimSpace(text) == "" {
return "", "", fmt.Errorf("SKILL.md is empty")
}
lines := strings.Split(text, "\n")
if len(lines) < 2 || strings.TrimSpace(lines[0]) != "---" {
return "", "", fmt.Errorf("SKILL.md must start with YAML front matter (---) per Agent Skills standard")
}
var fmLines []string
i := 1
for i < len(lines) {
if strings.TrimSpace(lines[i]) == "---" {
break
}
fmLines = append(fmLines, lines[i])
i++
}
if i >= len(lines) {
return "", "", fmt.Errorf("SKILL.md: front matter must end with a line containing only ---")
}
body = strings.Join(lines[i+1:], "\n")
body = strings.TrimSpace(body)
fmYAML = strings.Join(fmLines, "\n")
return fmYAML, body, nil
}
// ParseSkillMD parses SKILL.md YAML head + body.
func ParseSkillMD(raw []byte) (*SkillManifest, string, error) {
fmYAML, body, err := ExtractSkillMDFrontMatterYAML(raw)
if err != nil {
return nil, "", err
}
var m SkillManifest
if err := yaml.Unmarshal([]byte(fmYAML), &m); err != nil {
return nil, "", fmt.Errorf("SKILL.md front matter: %w", err)
}
return &m, body, nil
}
type skillFrontMatterExport struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
License string `yaml:"license,omitempty"`
Compatibility string `yaml:"compatibility,omitempty"`
Metadata map[string]any `yaml:"metadata,omitempty"`
AllowedTools string `yaml:"allowed-tools,omitempty"`
}
// BuildSkillMD serializes SKILL.md per agentskills.io.
func BuildSkillMD(m *SkillManifest, body string) ([]byte, error) {
if m == nil {
return nil, fmt.Errorf("nil manifest")
}
fm := skillFrontMatterExport{
Name: strings.TrimSpace(m.Name),
Description: strings.TrimSpace(m.Description),
License: strings.TrimSpace(m.License),
Compatibility: strings.TrimSpace(m.Compatibility),
AllowedTools: strings.TrimSpace(m.AllowedTools),
}
if len(m.Metadata) > 0 {
fm.Metadata = m.Metadata
}
head, err := yaml.Marshal(&fm)
if err != nil {
return nil, err
}
s := strings.TrimSpace(string(head))
out := "---\n" + s + "\n---\n\n" + strings.TrimSpace(body) + "\n"
return []byte(out), nil
}
func manifestTags(m *SkillManifest) []string {
if m == nil || m.Metadata == nil {
return nil
}
var out []string
if raw, ok := m.Metadata["tags"]; ok {
switch v := raw.(type) {
case []any:
for _, x := range v {
if s, ok := x.(string); ok && s != "" {
out = append(out, s)
}
}
case []string:
out = append(out, v...)
}
}
return out
}
func versionFromMetadata(m *SkillManifest) string {
if m == nil || m.Metadata == nil {
return ""
}
if v, ok := m.Metadata["version"]; ok {
if s, ok := v.(string); ok {
return strings.TrimSpace(s)
}
}
return ""
}
-200
View File
@@ -1,200 +0,0 @@
package skillpackage
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
const (
maxPackageFiles = 4000
maxPackageDepth = 24
maxScriptsDepth = 24
defaultMaxRead = 10 << 20
)
// SafeRelPath resolves rel inside root (no ..).
func SafeRelPath(root, rel string) (string, error) {
rel = strings.TrimSpace(rel)
rel = filepath.ToSlash(rel)
rel = strings.TrimPrefix(rel, "/")
if rel == "" || rel == "." {
return "", fmt.Errorf("empty resource path")
}
if strings.Contains(rel, "..") {
return "", fmt.Errorf("invalid path %q", rel)
}
abs := filepath.Join(root, filepath.FromSlash(rel))
cleanRoot := filepath.Clean(root)
cleanAbs := filepath.Clean(abs)
relOut, err := filepath.Rel(cleanRoot, cleanAbs)
if err != nil || relOut == ".." || strings.HasPrefix(relOut, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path escapes skill directory: %q", rel)
}
return cleanAbs, nil
}
// ListPackageFiles lists files under a skill directory.
func ListPackageFiles(skillsRoot, skillID string) ([]PackageFileInfo, error) {
root := SkillDir(skillsRoot, skillID)
if _, err := ResolveSKILLPath(root); err != nil {
return nil, fmt.Errorf("skill %q: %w", skillID, err)
}
var out []PackageFileInfo
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, e := filepath.Rel(root, path)
if e != nil {
return e
}
if rel == "." {
return nil
}
depth := strings.Count(rel, string(os.PathSeparator))
if depth > maxPackageDepth {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
if strings.HasPrefix(d.Name(), ".") {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
if len(out) >= maxPackageFiles {
return fmt.Errorf("skill package exceeds %d files", maxPackageFiles)
}
fi, err := d.Info()
if err != nil {
return err
}
out = append(out, PackageFileInfo{
Path: filepath.ToSlash(rel),
Size: fi.Size(),
IsDir: d.IsDir(),
})
return nil
})
return out, err
}
// ReadPackageFile reads a file relative to the skill package.
func ReadPackageFile(skillsRoot, skillID, relPath string, maxBytes int64) ([]byte, error) {
if maxBytes <= 0 {
maxBytes = defaultMaxRead
}
root := SkillDir(skillsRoot, skillID)
abs, err := SafeRelPath(root, relPath)
if err != nil {
return nil, err
}
fi, err := os.Stat(abs)
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("path is a directory")
}
if fi.Size() > maxBytes {
return readFileHead(abs, maxBytes)
}
return os.ReadFile(abs)
}
// WritePackageFile writes a file inside the skill package.
func WritePackageFile(skillsRoot, skillID, relPath string, content []byte) error {
root := SkillDir(skillsRoot, skillID)
if _, err := ResolveSKILLPath(root); err != nil {
return fmt.Errorf("skill %q: %w", skillID, err)
}
abs, err := SafeRelPath(root, relPath)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
return err
}
return os.WriteFile(abs, content, 0644)
}
func readFileHead(path string, max int64) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
buf := make([]byte, max)
n, err := f.Read(buf)
if err != nil && n == 0 {
return nil, err
}
return buf[:n], nil
}
func listScripts(skillsRoot, skillID string) ([]SkillScriptInfo, error) {
root := filepath.Join(SkillDir(skillsRoot, skillID), "scripts")
st, err := os.Stat(root)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
if !st.IsDir() {
return nil, nil
}
var out []SkillScriptInfo
err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
rel, e := filepath.Rel(root, path)
if e != nil {
return e
}
if rel == "." {
return nil
}
if d.IsDir() {
if strings.HasPrefix(d.Name(), ".") {
return filepath.SkipDir
}
if strings.Count(rel, string(os.PathSeparator)) >= maxScriptsDepth {
return filepath.SkipDir
}
return nil
}
if strings.HasPrefix(d.Name(), ".") {
return nil
}
relSkill := filepath.Join("scripts", rel)
full := filepath.Join(root, rel)
fi, err := os.Stat(full)
if err != nil || fi.IsDir() {
return nil
}
out = append(out, SkillScriptInfo{
Name: filepath.Base(rel),
RelPath: filepath.ToSlash(relSkill),
Size: fi.Size(),
})
return nil
})
return out, err
}
func countNonDirFiles(files []PackageFileInfo) int {
n := 0
for _, f := range files {
if !f.IsDir && f.Path != "SKILL.md" {
n++
}
}
return n
}
-66
View File
@@ -1,66 +0,0 @@
package skillpackage
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// SkillDir returns the absolute path to a skill package directory.
func SkillDir(skillsRoot, skillID string) string {
return filepath.Join(skillsRoot, skillID)
}
// ResolveSKILLPath returns SKILL.md path or error if missing.
func ResolveSKILLPath(skillPath string) (string, error) {
md := filepath.Join(skillPath, "SKILL.md")
if st, err := os.Stat(md); err != nil || st.IsDir() {
return "", fmt.Errorf("missing SKILL.md in %q (Agent Skills standard)", filepath.Base(skillPath))
}
return md, nil
}
// SkillsRootFromConfig resolves cfg.SkillsDir relative to the config file directory.
func SkillsRootFromConfig(skillsDir string, configPath string) string {
if skillsDir == "" {
skillsDir = "skills"
}
configDir := filepath.Dir(configPath)
if !filepath.IsAbs(skillsDir) {
skillsDir = filepath.Join(configDir, skillsDir)
}
return skillsDir
}
// DirLister satisfies handler.SkillsManager for role UI (lists package directory names).
type DirLister struct {
SkillsRoot string
}
// ListSkills implements the role handler dependency.
func (d DirLister) ListSkills() ([]string, error) {
return ListSkillDirNames(d.SkillsRoot)
}
// ListSkillDirNames returns subdirectory names under skillsRoot that contain SKILL.md.
func ListSkillDirNames(skillsRoot string) ([]string, error) {
if _, err := os.Stat(skillsRoot); os.IsNotExist(err) {
return nil, nil
}
entries, err := os.ReadDir(skillsRoot)
if err != nil {
return nil, fmt.Errorf("read skills directory: %w", err)
}
var names []string
for _, entry := range entries {
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
continue
}
skillPath := filepath.Join(skillsRoot, entry.Name())
if _, err := ResolveSKILLPath(skillPath); err == nil {
names = append(names, entry.Name())
}
}
return names, nil
}
-155
View File
@@ -1,155 +0,0 @@
package skillpackage
import (
"fmt"
"os"
"sort"
"strings"
)
// ListSkillSummaries scans skillsRoot and returns index rows for the admin API.
func ListSkillSummaries(skillsRoot string) ([]SkillSummary, error) {
names, err := ListSkillDirNames(skillsRoot)
if err != nil {
return nil, err
}
sort.Strings(names)
out := make([]SkillSummary, 0, len(names))
for _, dirName := range names {
su, err := loadSummary(skillsRoot, dirName)
if err != nil {
continue
}
out = append(out, su)
}
return out, nil
}
func loadSummary(skillsRoot, dirName string) (SkillSummary, error) {
skillPath := SkillDir(skillsRoot, dirName)
mdPath, err := ResolveSKILLPath(skillPath)
if err != nil {
return SkillSummary{}, err
}
raw, err := os.ReadFile(mdPath)
if err != nil {
return SkillSummary{}, err
}
man, _, err := ParseSkillMD(raw)
if err != nil {
return SkillSummary{}, err
}
if err := ValidateAgentSkillManifestInPackage(man, dirName); err != nil {
return SkillSummary{}, err
}
fi, err := os.Stat(mdPath)
if err != nil {
return SkillSummary{}, err
}
pfiles, err := ListPackageFiles(skillsRoot, dirName)
if err != nil {
return SkillSummary{}, err
}
nFiles := 0
for _, p := range pfiles {
if !p.IsDir {
nFiles++
}
}
scripts, err := listScripts(skillsRoot, dirName)
if err != nil {
return SkillSummary{}, err
}
ver := versionFromMetadata(man)
return SkillSummary{
ID: dirName,
DirName: dirName,
Name: man.Name,
Description: man.Description,
Version: ver,
Path: skillPath,
Tags: manifestTags(man),
ScriptCount: len(scripts),
FileCount: nFiles,
FileSize: fi.Size(),
ModTime: fi.ModTime().Format("2006-01-02 15:04:05"),
Progressive: true,
}, nil
}
// LoadOptions mirrors legacy API query params for the web admin.
type LoadOptions struct {
Depth string // summary | full
Section string
}
// LoadSkill returns manifest + body + package listing for admin.
func LoadSkill(skillsRoot, skillID string, opt LoadOptions) (*SkillView, error) {
skillPath := SkillDir(skillsRoot, skillID)
mdPath, err := ResolveSKILLPath(skillPath)
if err != nil {
return nil, err
}
raw, err := os.ReadFile(mdPath)
if err != nil {
return nil, err
}
man, body, err := ParseSkillMD(raw)
if err != nil {
return nil, err
}
if err := ValidateAgentSkillManifestInPackage(man, skillID); err != nil {
return nil, err
}
pfiles, err := ListPackageFiles(skillsRoot, skillID)
if err != nil {
return nil, err
}
scripts, err := listScripts(skillsRoot, skillID)
if err != nil {
return nil, err
}
sort.Slice(scripts, func(i, j int) bool { return scripts[i].RelPath < scripts[j].RelPath })
sections := deriveSections(body)
ver := versionFromMetadata(man)
v := &SkillView{
DirName: skillID,
Name: man.Name,
Description: man.Description,
Content: body,
Path: skillPath,
Version: ver,
Tags: manifestTags(man),
Scripts: scripts,
Sections: sections,
PackageFiles: pfiles,
}
depth := strings.ToLower(strings.TrimSpace(opt.Depth))
if depth == "" {
depth = "full"
}
sec := strings.TrimSpace(opt.Section)
if sec != "" {
mds := splitMarkdownSections(body)
chunk := findSectionContent(mds, sec)
if chunk == "" {
v.Content = fmt.Sprintf("_(section %q not found in SKILL.md for skill %s)_", sec, skillID)
} else {
v.Content = chunk
}
return v, nil
}
if depth == "summary" {
v.Content = buildSummaryMarkdown(man.Name, man.Description, v.Tags, scripts, sections, body)
}
return v, nil
}
// ReadScriptText returns file content as string (for HTTP resource_path).
func ReadScriptText(skillsRoot, skillID, relPath string, maxBytes int64) (string, error) {
b, err := ReadPackageFile(skillsRoot, skillID, relPath, maxBytes)
if err != nil {
return "", err
}
return string(b), nil
}
-67
View File
@@ -1,67 +0,0 @@
// Package skillpackage provides filesystem-backed Agent Skills layout (SKILL.md + package files)
// for HTTP admin APIs. Runtime discovery and progressive loading for agents use Eino ADK skill middleware.
package skillpackage
// SkillManifest is parsed from SKILL.md front matter (https://agentskills.io/specification.md).
type SkillManifest struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
License string `yaml:"license,omitempty"`
Compatibility string `yaml:"compatibility,omitempty"`
Metadata map[string]any `yaml:"metadata,omitempty"`
AllowedTools string `yaml:"allowed-tools,omitempty"`
}
// SkillSummary is API metadata for one skill directory.
type SkillSummary struct {
ID string `json:"id"`
DirName string `json:"dir_name"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Path string `json:"path"`
Tags []string `json:"tags"`
Triggers []string `json:"triggers,omitempty"`
ScriptCount int `json:"script_count"`
FileCount int `json:"file_count"`
FileSize int64 `json:"file_size"`
ModTime string `json:"mod_time"`
Progressive bool `json:"progressive"`
}
// SkillScriptInfo describes a file under scripts/.
type SkillScriptInfo struct {
Name string `json:"name"`
RelPath string `json:"rel_path"`
Description string `json:"description,omitempty"`
Size int64 `json:"size"`
}
// SkillSection is derived from ## headings in SKILL.md.
type SkillSection struct {
ID string `json:"id"`
Title string `json:"title"`
Heading string `json:"heading"`
Level int `json:"level"`
}
// PackageFileInfo describes one file inside a package.
type PackageFileInfo struct {
Path string `json:"path"`
Size int64 `json:"size"`
IsDir bool `json:"is_dir,omitempty"`
}
// SkillView is a loaded package for admin / API.
type SkillView struct {
DirName string `json:"dir_name"`
Name string `json:"name"`
Description string `json:"description"`
Content string `json:"content"`
Path string `json:"path"`
Version string `json:"version"`
Tags []string `json:"tags"`
Scripts []SkillScriptInfo `json:"scripts,omitempty"`
Sections []SkillSection `json:"sections,omitempty"`
PackageFiles []PackageFileInfo `json:"package_files,omitempty"`
}
-102
View File
@@ -1,102 +0,0 @@
package skillpackage
import (
"fmt"
"strings"
"unicode/utf8"
"gopkg.in/yaml.v3"
)
var agentSkillsSpecFrontMatterKeys = map[string]struct{}{
"name": {}, "description": {}, "license": {}, "compatibility": {},
"metadata": {}, "allowed-tools": {},
}
// ValidateAgentSkillManifest enforces Agent Skills rules for name and description.
func ValidateAgentSkillManifest(m *SkillManifest) error {
if m == nil {
return fmt.Errorf("skill manifest is nil")
}
if strings.TrimSpace(m.Name) == "" {
return fmt.Errorf("SKILL.md front matter: name is required")
}
if strings.TrimSpace(m.Description) == "" {
return fmt.Errorf("SKILL.md front matter: description is required")
}
if utf8.RuneCountInString(m.Name) > 64 {
return fmt.Errorf("name exceeds 64 characters (Agent Skills limit)")
}
if utf8.RuneCountInString(m.Description) > 1024 {
return fmt.Errorf("description exceeds 1024 characters (Agent Skills limit)")
}
if m.Name != strings.ToLower(m.Name) {
return fmt.Errorf("name must be lowercase (Agent Skills)")
}
for _, r := range m.Name {
if !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-') {
return fmt.Errorf("name must contain only lowercase letters, numbers, hyphens (Agent Skills)")
}
}
if strings.HasPrefix(m.Name, "-") || strings.HasSuffix(m.Name, "-") {
return fmt.Errorf("name must not start or end with a hyphen (Agent Skills spec)")
}
if strings.Contains(m.Name, "--") {
return fmt.Errorf("name must not contain consecutive hyphens (Agent Skills spec)")
}
lname := strings.ToLower(m.Name)
if strings.Contains(lname, "anthropic") || strings.Contains(lname, "claude") {
return fmt.Errorf("name must not contain reserved words anthropic or claude")
}
return nil
}
// ValidateAgentSkillManifestInPackage checks manifest and that name matches package directory.
func ValidateAgentSkillManifestInPackage(m *SkillManifest, packageDirName string) error {
if err := ValidateAgentSkillManifest(m); err != nil {
return err
}
if strings.TrimSpace(packageDirName) == "" {
return nil
}
if m.Name != packageDirName {
return fmt.Errorf("SKILL.md name %q must match directory name %q (Agent Skills spec)", m.Name, packageDirName)
}
return nil
}
// ValidateOfficialFrontMatterTopLevelKeys rejects keys not in the open spec.
func ValidateOfficialFrontMatterTopLevelKeys(fmYAML string) error {
var top map[string]interface{}
if err := yaml.Unmarshal([]byte(fmYAML), &top); err != nil {
return fmt.Errorf("SKILL.md front matter: %w", err)
}
for k := range top {
if _, ok := agentSkillsSpecFrontMatterKeys[k]; !ok {
return fmt.Errorf("SKILL.md front matter: unsupported key %q (allowed: name, description, license, compatibility, metadata, allowed-tools — see https://agentskills.io/specification.md)", k)
}
}
return nil
}
// ValidateSkillMDPackage validates SKILL.md bytes for writes.
func ValidateSkillMDPackage(raw []byte, packageDirName string) error {
fmYAML, body, err := ExtractSkillMDFrontMatterYAML(raw)
if err != nil {
return err
}
if err := ValidateOfficialFrontMatterTopLevelKeys(fmYAML); err != nil {
return err
}
if strings.TrimSpace(body) == "" {
return fmt.Errorf("SKILL.md: markdown body after front matter must not be empty")
}
var fm SkillManifest
if err := yaml.Unmarshal([]byte(fmYAML), &fm); err != nil {
return fmt.Errorf("SKILL.md front matter: %w", err)
}
if c := strings.TrimSpace(fm.Compatibility); c != "" && utf8.RuneCountInString(c) > 500 {
return fmt.Errorf("compatibility exceeds 500 characters (Agent Skills spec)")
}
return ValidateAgentSkillManifestInPackage(&fm, packageDirName)
}