Skip to content

Commit 8d2b43d

Browse files
authored
Merge pull request #1139 from rumpl/skills-spec
Adhere to the skills spec
2 parents 0c41558 + 619e155 commit 8d2b43d

File tree

3 files changed

+282
-37
lines changed

3 files changed

+282
-37
lines changed

golang_developer.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ agents:
9494
- type: filesystem
9595
- type: shell
9696
- type: todo
97+
- type: fetch
9798
sub_agents:
9899
- librarian
99100

pkg/skills/skills.go

Lines changed: 114 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,32 @@ const (
1616
formatCodex skillFormat = "codex"
1717

1818
skillFile = "SKILL.md"
19+
20+
maxNameLength = 64
21+
maxDescriptionLength = 1024
22+
maxCompatLength = 500
1923
)
2024

25+
var namePattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
26+
2127
type Skill struct {
22-
Name string
23-
Description string
24-
FilePath string
25-
BaseDir string
28+
Name string
29+
Description string
30+
FilePath string
31+
BaseDir string
32+
License string
33+
Compatibility string
34+
Metadata map[string]string
35+
AllowedTools []string
2636
}
2737

2838
type frontmatter struct {
29-
Name string
30-
Description string
39+
Name string
40+
Description string
41+
License string
42+
Compatibility string
43+
Metadata map[string]string
44+
AllowedTools []string
3145
}
3246

3347
func stripQuotes(value string) string {
@@ -40,6 +54,13 @@ func stripQuotes(value string) string {
4054
return value
4155
}
4256

57+
func isValidName(name string) bool {
58+
if name == "" || len(name) > maxNameLength {
59+
return false
60+
}
61+
return namePattern.MatchString(name)
62+
}
63+
4364
func parseFrontmatter(content string) (frontmatter, string) {
4465
fm := frontmatter{}
4566

@@ -58,8 +79,25 @@ func parseFrontmatter(content string) (frontmatter, string) {
5879
frontmatterBlock := normalizedContent[4 : endIndex+3]
5980
body := strings.TrimSpace(normalizedContent[endIndex+7:])
6081

61-
lineRegex := regexp.MustCompile(`^(\w+):\s*(.*)$`)
82+
lineRegex := regexp.MustCompile(`^([\w-]+):\s*(.*)$`)
83+
metadataRegex := regexp.MustCompile(`^\s+(\w+):\s*(.*)$`)
84+
inMetadata := false
85+
6286
for line := range strings.SplitSeq(frontmatterBlock, "\n") {
87+
if inMetadata {
88+
matches := metadataRegex.FindStringSubmatch(line)
89+
if matches != nil {
90+
key := matches[1]
91+
value := stripQuotes(strings.TrimSpace(matches[2]))
92+
if fm.Metadata == nil {
93+
fm.Metadata = make(map[string]string)
94+
}
95+
fm.Metadata[key] = value
96+
continue
97+
}
98+
inMetadata = false
99+
}
100+
63101
matches := lineRegex.FindStringSubmatch(line)
64102
if matches != nil {
65103
key := matches[1]
@@ -69,6 +107,17 @@ func parseFrontmatter(content string) (frontmatter, string) {
69107
fm.Name = value
70108
case "description":
71109
fm.Description = value
110+
case "license":
111+
fm.License = value
112+
case "compatibility":
113+
fm.Compatibility = value
114+
case "metadata":
115+
inMetadata = true
116+
fm.Metadata = make(map[string]string)
117+
case "allowed-tools":
118+
if value != "" {
119+
fm.AllowedTools = strings.Fields(value)
120+
}
72121
}
73122
}
74123
}
@@ -106,14 +155,14 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
106155
continue
107156
}
108157

109-
skillFile := filepath.Join(fullPath, skillFile)
110-
rawContent, err := os.ReadFile(skillFile)
158+
skillFilePath := filepath.Join(fullPath, skillFile)
159+
rawContent, err := os.ReadFile(skillFilePath)
111160
if err != nil {
112161
continue
113162
}
114163

115164
fm, _ := parseFrontmatter(string(rawContent))
116-
if fm.Description == "" {
165+
if !isValidFrontmatter(fm, entry.Name()) {
117166
continue
118167
}
119168

@@ -123,10 +172,14 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
123172
}
124173

125174
skills = append(skills, Skill{
126-
Name: name,
127-
Description: fm.Description,
128-
FilePath: skillFile,
129-
BaseDir: fullPath,
175+
Name: name,
176+
Description: fm.Description,
177+
FilePath: skillFilePath,
178+
BaseDir: fullPath,
179+
License: fm.License,
180+
Compatibility: fm.Compatibility,
181+
Metadata: fm.Metadata,
182+
AllowedTools: fm.AllowedTools,
130183
})
131184

132185
case formatCodex:
@@ -138,22 +191,28 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
138191
continue
139192
}
140193

194+
skillDir := filepath.Dir(fullPath)
195+
dirName := filepath.Base(skillDir)
196+
141197
fm, _ := parseFrontmatter(string(rawContent))
142-
if fm.Description == "" {
198+
if !isValidFrontmatter(fm, dirName) {
143199
continue
144200
}
145201

146-
skillDir := filepath.Dir(fullPath)
147202
name := fm.Name
148203
if name == "" {
149-
name = filepath.Base(skillDir)
204+
name = dirName
150205
}
151206

152207
skills = append(skills, Skill{
153-
Name: name,
154-
Description: fm.Description,
155-
FilePath: fullPath,
156-
BaseDir: skillDir,
208+
Name: name,
209+
Description: fm.Description,
210+
FilePath: fullPath,
211+
BaseDir: skillDir,
212+
License: fm.License,
213+
Compatibility: fm.Compatibility,
214+
Metadata: fm.Metadata,
215+
AllowedTools: fm.AllowedTools,
157216
})
158217
}
159218
}
@@ -162,6 +221,27 @@ func loadSkillsFromDir(dir string, format skillFormat) []Skill {
162221
return skills
163222
}
164223

224+
func isValidFrontmatter(fm frontmatter, dirName string) bool {
225+
if fm.Description == "" || len(fm.Description) > maxDescriptionLength {
226+
return false
227+
}
228+
229+
if fm.Compatibility != "" && len(fm.Compatibility) > maxCompatLength {
230+
return false
231+
}
232+
233+
if fm.Name != "" {
234+
if !isValidName(fm.Name) {
235+
return false
236+
}
237+
if fm.Name != dirName {
238+
return false
239+
}
240+
}
241+
242+
return true
243+
}
244+
165245
func Load() []Skill {
166246
skillMap := make(map[string]Skill)
167247

@@ -202,21 +282,24 @@ func BuildSkillsPrompt(skills []Skill) string {
202282
}
203283

204284
var sb strings.Builder
205-
sb.WriteString("\n\n<available_skills>\n")
206-
sb.WriteString("The following skills provide specialized instructions for specific tasks.\n")
207-
sb.WriteString("Use the read_file tool to load a skill's file when the task matches its description.\n")
208-
sb.WriteString("Skills may contain {baseDir} placeholders - replace them with the skill's base directory path.\n\n")
285+
sb.WriteString("The following skills provide specialized instructions for specific tasks. ")
286+
sb.WriteString("Each skill's description indicates what it does and when to use it.\n\n")
287+
sb.WriteString("When a user's request matches a skill's description, use the read_file tool to load the skill's SKILL.md file from the location path. ")
288+
sb.WriteString("The file contains detailed instructions to follow for that task.\n\n")
209289

290+
sb.WriteString("\n\n<available_skills>\n")
210291
for _, skill := range skills {
211-
sb.WriteString("- ")
292+
sb.WriteString(" <skill>\n")
293+
sb.WriteString(" <name>")
212294
sb.WriteString(skill.Name)
213-
sb.WriteString(": ")
295+
sb.WriteString("</name>\n")
296+
sb.WriteString(" <description>")
214297
sb.WriteString(skill.Description)
215-
sb.WriteString("\n File: ")
298+
sb.WriteString("</description>\n")
299+
sb.WriteString(" <location>")
216300
sb.WriteString(skill.FilePath)
217-
sb.WriteString("\n Base directory: ")
218-
sb.WriteString(skill.BaseDir)
219-
sb.WriteString("\n")
301+
sb.WriteString("</location>\n")
302+
sb.WriteString(" </skill>\n")
220303
}
221304

222305
sb.WriteString("</available_skills>")

0 commit comments

Comments
 (0)