-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate_skills.py
More file actions
162 lines (132 loc) · 5.23 KB
/
Copy pathvalidate_skills.py
File metadata and controls
162 lines (132 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python3
"""Validate that every skill in this repository is loadable by SKILL.md agents.
This is a deterministic, offline check of the cross-agent Agent Skills format
(https://developers.openai.com/codex/skills and the Claude Code skills format).
It is meant to run in CI without any human interaction, API keys, or network:
- each skill is a flat directory with a top-level SKILL.md
- SKILL.md has YAML frontmatter with non-empty ``name`` and ``description``
- the frontmatter ``name`` matches the skill's directory name
- no SKILL.md is nested inside another skill (agents like Claude Code that
load a flat skills directory cannot install nested skills)
- an optional ``agents/openai.yaml`` parses and uses only known top-level keys
- script paths referenced from SKILL.md as ``skills/<name>/scripts/...`` exist
Exit code is non-zero if any skill fails, and every problem is printed.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS_ROOT = REPO_ROOT / "skills"
KNOWN_OPENAI_KEYS = {"interface", "policy", "dependencies"}
SCRIPT_REF = re.compile(r"skills/[A-Za-z0-9_.-]+/scripts/[A-Za-z0-9_./-]+\.py")
def parse_frontmatter(text: str) -> dict[str, str]:
"""Parse a YAML frontmatter block, preferring PyYAML when available."""
if not text.startswith("---"):
return {}
end = text.find("\n---", 3)
if end == -1:
return {}
block = text[3:end].strip("\n")
try:
import yaml # type: ignore
data = yaml.safe_load(block)
if isinstance(data, dict):
return {
str(key): "" if value is None else str(value)
for key, value in data.items()
}
except Exception:
# Fall back to the minimal hand-rolled parser below.
pass
fields: dict[str, str] = {}
for line in block.splitlines():
if not line.strip() or line.lstrip().startswith("#"):
continue
if ":" not in line or line[0] in " \t":
continue
key, _, value = line.partition(":")
fields[key.strip()] = value.strip().strip("'\"")
return fields
def validate_skill(skill_dir: Path) -> list[str]:
problems: list[str] = []
name = skill_dir.name
skill_md = skill_dir / "SKILL.md"
if not skill_md.is_file():
return [f"{name}: missing SKILL.md"]
nested = [
p for p in skill_dir.rglob("SKILL.md") if p != skill_md
]
for path in nested:
problems.append(f"{name}: nested skill at {path.relative_to(SKILLS_ROOT)}")
text = skill_md.read_text()
fields = parse_frontmatter(text)
if not fields.get("name"):
problems.append(f"{name}: SKILL.md frontmatter missing 'name'")
elif fields["name"] != name:
problems.append(
f"{name}: frontmatter name '{fields['name']}' != directory '{name}'"
)
if not fields.get("description"):
problems.append(f"{name}: SKILL.md frontmatter missing 'description'")
openai_yaml = skill_dir / "agents" / "openai.yaml"
if openai_yaml.is_file():
problems.extend(_validate_openai_yaml(name, openai_yaml))
for match in SCRIPT_REF.findall(text):
if not (REPO_ROOT / match).is_file():
problems.append(f"{name}: SKILL.md references missing script {match}")
return problems
def _validate_openai_yaml(name: str, path: Path) -> list[str]:
try:
import yaml # type: ignore
except ImportError:
# PyYAML may be absent; fall back to a top-level key sniff.
top_keys = {
line.split(":", 1)[0].strip()
for line in path.read_text().splitlines()
if line and line[0] not in " \t#" and ":" in line
}
unknown = top_keys - KNOWN_OPENAI_KEYS
return (
[f"{name}: agents/openai.yaml unknown top-level keys: {sorted(unknown)}"]
if unknown
else []
)
try:
data = yaml.safe_load(path.read_text())
except yaml.YAMLError as exc:
return [f"{name}: agents/openai.yaml does not parse: {exc}"]
if not isinstance(data, dict):
return [f"{name}: agents/openai.yaml must be a mapping"]
unknown = set(data) - KNOWN_OPENAI_KEYS
return (
[f"{name}: agents/openai.yaml unknown top-level keys: {sorted(unknown)}"]
if unknown
else []
)
def discover_skills(skills_root: Path = SKILLS_ROOT) -> list[Path]:
return sorted(
child
for child in skills_root.iterdir()
if child.is_dir() and (child / "SKILL.md").is_file()
)
def main() -> int:
skills = discover_skills()
if not skills:
print("error: no skills found under skills/", file=sys.stderr)
return 2
all_problems: list[str] = []
for skill_dir in skills:
problems = validate_skill(skill_dir)
status = "FAIL" if problems else "ok"
print(f"[{status}] {skill_dir.name}")
all_problems.extend(problems)
if all_problems:
print("\nProblems:", file=sys.stderr)
for problem in all_problems:
print(f" - {problem}", file=sys.stderr)
return 1
print(f"\nAll {len(skills)} skills valid.")
return 0
if __name__ == "__main__":
raise SystemExit(main())