Setting the file. One moment.
Quick Validate · Skill Writer · getsentry/skills · Skills Docs
ContentsBack to the top of the page 23.10
Example Hook Backed Skill
scripts/ quick_validate.py
Python · 154 lines · 5 KB
17
"""
18
19 import argparse
20 import json
21 import re
22 import sys
23 from pathlib import Path
24
25 import yaml
26
27 MAX_SKILL_CHARS = 20000
28
29 LOCAL_FILE_REFERENCE_RE = re.compile(
30 r " (?<! [ A-Za-z0-9_./- ] ) "
31 r " ((?: references | scripts | assets ) / [ A-Za-z0-9 ][ A-Za-z0-9._/- ] * \. [ A-Za-z0-9 ] +| "
32 r " (?: SPEC | SOURCES ) \. md)"
33 r " (?! [ A-Za-z0-9_./- ] ) "
34 )
35
36
37 def parse_args (argv: list[ str ]) -> argparse.Namespace:
38 parser = argparse.ArgumentParser(
39 description = "Validate the structural requirements for an agent skill." ,
40 )
41 parser.add_argument( "skill_directory" )
42 return parser.parse_args(argv)
43
44
45 def find_local_file_references (text: str ) -> list[ str ]:
46 refs: list[ str ] = []
47 for match in LOCAL_FILE_REFERENCE_RE .finditer(text):
48 ref = match.group( 1 )
49 if ref not in refs:
50 refs.append(ref)
51 return refs
52
53
54 def validate_local_file_references (
55 skill_path: Path,
56 skill_content: str ,
57 errors: list[ str ],
58 ) -> None :
59 for rel_path in find_local_file_references(skill_content):
60 target = skill_path / rel_path
61 if not target.exists():
62 errors.append( f "Referenced file not found: { rel_path } " )
63 elif not target.is_file():
64 errors.append( f "Referenced path is not a file: { rel_path } " )
65
66
67 def validate_skill (
68 skill_path: Path,
69 ) -> tuple[ bool , list[ str ], list[ str ]]:
70 """Validate a skill directory. Returns (valid, errors, warnings)."""
71 errors: list[ str ] = []
72 warnings: list[ str ] = []
73
74 skill_md = skill_path / "SKILL.md"
75 if not skill_md.exists():
76 return False , [ "SKILL.md not found" ], []
77
78 content = skill_md.read_text()
79
80 if not content.startswith( "---" ):
81 errors.append( "No YAML frontmatter found (file must start with ---)" )
82 return False , errors, warnings
83
84 match = re.match( r " ^ --- \n (. *? ) \n ---" , content, re. DOTALL )
85 if not match:
86 errors.append( "Invalid frontmatter format (missing closing ---)" )
87 return False , errors, warnings
88
89 frontmatter_text = match.group( 1 )
90 try :
91 frontmatter = yaml.safe_load(frontmatter_text)
92 if not isinstance (frontmatter, dict ):
93 errors.append( "Frontmatter must be a YAML mapping" )
94 return False , errors, warnings
95 except yaml.YAMLError as exc:
96 errors.append( f "Invalid YAML in frontmatter: { exc } " )
97 return False , errors, warnings
98
99 invalid_keys = [key for key in frontmatter.keys() if not isinstance (key, str ) or not key.strip()]
100 if invalid_keys:
101 errors.append( "Frontmatter keys must be non-empty strings" )
102
103 if "name" not in frontmatter:
104 errors.append( "Missing required field: name" )
105 else :
106 name = frontmatter[ "name" ]
107 if not isinstance (name, str ):
108 errors.append( f "name must be a string, got { type (name). __name__ } " )
109 else :
110 name = name.strip()
111 if not name:
112 errors.append( "name must not be empty" )
113 elif name != skill_path.name:
114 errors.append( f "name ' { name } ' does not match directory name ' { skill_path.name } '" )
115
116 if "description" not in frontmatter:
117 errors.append( "Missing required field: description" )
118 else :
119 description = frontmatter[ "description" ]
120 if not isinstance (description, str ):
121 errors.append( f "description must be a string, got { type (description). __name__ } " )
122 elif not description.strip():
123 errors.append( "description must not be empty" )
124
125 if len (content) > MAX_SKILL_CHARS :
126 warnings.append(
127 f "SKILL.md is { len (content) } characters (recommended max { MAX_SKILL_CHARS } ). "
128 "Consider moving optional detail to references/."
129 )
130
131 validate_local_file_references(skill_path, content, errors)
132
133 return len (errors) == 0 , errors, warnings
134
135
136 def main () -> None :
137 args = parse_args(sys.argv[ 1 :])
138 skill_path = Path(args.skill_directory).resolve()
139 if not skill_path.is_dir():
140 print (json.dumps({ "valid" : False , "errors" : [ f "Not a directory: { skill_path } " ]}))
141 sys.exit( 1 )
142
143 valid, errors, warnings = validate_skill(skill_path)
144 result = {
145 "valid" : valid,
146 "errors" : errors,
147 "warnings" : warnings,
148 }
149 print (json.dumps(result, indent = 2 ))
150 sys.exit( 0 if valid else 1 )
151
152
153 if __name__ == "__main__" :
154 main()