Skip to content

Commit 3547aef

Browse files
authored
Merge pull request #11 from gordonmurray/update-skills-july-2026
Restructure skills as operating guides, verify July 2026 versions, add CI validation
2 parents 2269735 + f4f3a2a commit 3547aef

11 files changed

Lines changed: 913 additions & 138 deletions

File tree

.github/scripts/validate_skills.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env python3
2+
"""Validate the skill catalogue against this repository's structural rules.
3+
4+
Checks frontmatter validity, naming, required sections, file size, directory
5+
layout, relative links, and README coverage. Run before opening a pull request:
6+
7+
python3 .github/scripts/validate_skills.py
8+
9+
Exits 0 when every skill passes and 1 otherwise.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
import pathlib
16+
import re
17+
import sys
18+
19+
try:
20+
import yaml
21+
except ImportError: # pragma: no cover
22+
sys.exit("PyYAML is required. Install it with: pip install pyyaml")
23+
24+
REPO = pathlib.Path(__file__).resolve().parents[2]
25+
26+
MAX_DESCRIPTION = 1024
27+
MAX_NAME = 64
28+
MAX_LINES = 500
29+
30+
NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
31+
FRONTMATTER = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL)
32+
SECTION = re.compile(r"^## (.+)$", re.MULTILINE)
33+
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
34+
35+
REQUIRED_SECTIONS = (
36+
"Scope",
37+
"Inspect First",
38+
"Safety",
39+
"Verify",
40+
"Update Checklist",
41+
)
42+
43+
# A skill directory holds only resources the agent uses to perform the skill.
44+
# Anything else, authoring notes included, is clutter. Contents of the resource
45+
# directories are not policed by name; dotfiles are ignored as tooling.
46+
ALLOWED_FILES = ("SKILL.md",)
47+
ALLOWED_DIRS = ("references", "scripts", "assets")
48+
LAYOUT_HINT = "a skill directory holds only SKILL.md plus references/, scripts/, and assets/"
49+
50+
51+
def skill_dirs(root: pathlib.Path) -> list[pathlib.Path]:
52+
return sorted(
53+
p for p in root.iterdir() if p.is_dir() and not p.name.startswith(".")
54+
)
55+
56+
57+
def check_skill(directory: pathlib.Path) -> tuple[list[str], str | None, int, int]:
58+
"""Return (errors, skill name, line count, description length)."""
59+
errors: list[str] = []
60+
skill_md = directory / "SKILL.md"
61+
62+
if not skill_md.exists():
63+
return ([f"no SKILL.md in {directory.name}/"], None, 0, 0)
64+
65+
for entry in sorted(directory.iterdir()):
66+
if entry.name.startswith("."):
67+
continue
68+
if entry.is_dir():
69+
if entry.name not in ALLOWED_DIRS:
70+
errors.append(f"unexpected directory {entry.name}/; {LAYOUT_HINT}")
71+
elif entry.name not in ALLOWED_FILES:
72+
errors.append(f"unexpected file {entry.name}; {LAYOUT_HINT}")
73+
74+
text = skill_md.read_text(encoding="utf-8")
75+
line_count = len(text.splitlines())
76+
if line_count > MAX_LINES:
77+
errors.append(f"SKILL.md is {line_count} lines, over the {MAX_LINES} line ceiling")
78+
79+
match = FRONTMATTER.match(text)
80+
if not match:
81+
errors.append("missing YAML frontmatter delimited by --- on the first line")
82+
return (errors, None, line_count, 0)
83+
84+
try:
85+
meta = yaml.safe_load(match.group(1))
86+
except yaml.YAMLError as exc:
87+
errors.append(f"frontmatter is not valid YAML: {exc}")
88+
return (errors, None, line_count, 0)
89+
90+
if not isinstance(meta, dict):
91+
errors.append("frontmatter must be a YAML mapping")
92+
return (errors, None, line_count, 0)
93+
94+
name = meta.get("name")
95+
if not isinstance(name, str) or not name:
96+
errors.append("frontmatter is missing a name")
97+
name = None
98+
else:
99+
if name != directory.name:
100+
errors.append(f'name "{name}" does not match directory "{directory.name}"')
101+
if not NAME_PATTERN.match(name):
102+
errors.append(f'name "{name}" must be lowercase kebab case')
103+
if len(name) > MAX_NAME:
104+
errors.append(f"name is {len(name)} characters, over the {MAX_NAME} limit")
105+
106+
description = meta.get("description")
107+
desc_len = 0
108+
if not isinstance(description, str) or not description.strip():
109+
errors.append("frontmatter is missing a description")
110+
else:
111+
desc_len = len(description)
112+
if desc_len > MAX_DESCRIPTION:
113+
errors.append(
114+
f"description is {desc_len} characters, over the {MAX_DESCRIPTION} limit"
115+
)
116+
117+
sections = SECTION.findall(text)
118+
for required in REQUIRED_SECTIONS:
119+
if required not in sections:
120+
errors.append(f"missing required section: ## {required}")
121+
122+
for target in MD_LINK.findall(text):
123+
target = target.split("#", 1)[0].strip()
124+
if not target or "://" in target or target.startswith("mailto:"):
125+
continue
126+
if not (directory / target).exists():
127+
errors.append(f"broken relative link: {target}")
128+
129+
return (errors, name, line_count, desc_len)
130+
131+
132+
def check_readme(root: pathlib.Path, dir_names: list[str]) -> list[str]:
133+
readme = root / "README.md"
134+
if not readme.exists():
135+
return ["no README.md at the repository root"]
136+
137+
text = readme.read_text(encoding="utf-8")
138+
listed = {
139+
t.split("/", 1)[0]
140+
for t in MD_LINK.findall(text)
141+
if t.endswith("/SKILL.md") and "://" not in t
142+
}
143+
144+
errors = []
145+
for missing in sorted(set(dir_names) - listed):
146+
errors.append(f"README does not list the {missing} skill")
147+
for stale in sorted(listed - set(dir_names)):
148+
errors.append(f"README links to {stale}/SKILL.md, which does not exist")
149+
return errors
150+
151+
152+
def main() -> int:
153+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
154+
parser.add_argument(
155+
"--root",
156+
type=pathlib.Path,
157+
default=REPO,
158+
help="repository root to validate (default: this repository)",
159+
)
160+
args = parser.parse_args()
161+
162+
directories = skill_dirs(args.root)
163+
if not directories:
164+
print(f"No skill directories found in {args.root}")
165+
return 1
166+
167+
failed = 0
168+
dir_names: list[str] = []
169+
seen: dict[str, str] = {}
170+
171+
for directory in directories:
172+
errors, name, lines, desc_len = check_skill(directory)
173+
dir_names.append(directory.name)
174+
175+
if name:
176+
if name in seen:
177+
errors.append(f'duplicate skill name, also used by {seen[name]}/')
178+
seen[name] = directory.name
179+
180+
if errors:
181+
failed += 1
182+
print(f"FAIL {directory.name}")
183+
for error in errors:
184+
print(f" {error}")
185+
else:
186+
print(f"ok {directory.name:<16} {lines:>3} lines, description {desc_len} chars")
187+
188+
readme_errors = check_readme(args.root, dir_names)
189+
if readme_errors:
190+
failed += 1
191+
print("FAIL README.md")
192+
for error in readme_errors:
193+
print(f" {error}")
194+
195+
print()
196+
if failed:
197+
print(f"{failed} check group(s) failed")
198+
return 1
199+
200+
print(f"All {len(directories)} skills passed")
201+
return 0
202+
203+
204+
if __name__ == "__main__":
205+
sys.exit(main())
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Validate skills
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
validate:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: '3.x'
18+
19+
- name: Install dependencies
20+
run: pip install pyyaml
21+
22+
- name: Validate skill catalogue
23+
run: python3 .github/scripts/validate_skills.py

README.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# Data Engineering Skills for Claude
22

33
Expert knowledge skills for Claude Code and Claude.ai covering modern data
4-
engineering technologies table formats, stream processing, streaming storage,
4+
engineering technologies: table formats, stream processing, streaming storage,
55
ML-native formats, and local orchestration.
66

7-
Last reviewed and refreshed: June 2026.
8-
97
Each skill follows the
10-
[Anthropic Agent Skills](https://www.anthropic.com/news/skills) standard:
8+
[Anthropic Agent Skills](https://claude.com/blog/skills) standard:
119
a folder containing a `SKILL.md` with YAML frontmatter that Claude loads on
1210
demand when the trigger conditions match.
1311

@@ -21,16 +19,17 @@ demand when the trigger conditions match.
2119
| **Apache Flink** | Stream processing framework | [`flink/SKILL.md`](flink/SKILL.md) |
2220
| **Apache Iggy** | Rust-native message streaming | [`iggy/SKILL.md`](iggy/SKILL.md) |
2321
| **Lance** | Columnar format for ML/AI + vector search | [`lance/SKILL.md`](lance/SKILL.md) |
24-
| **Docker Compose** | Container orchestration (V2+) | [`docker-compose/SKILL.md`](docker-compose/SKILL.md) |
22+
| **Firn** | Object-storage-backed vector and full-text search | [`firn/SKILL.md`](firn/SKILL.md) |
23+
| **Docker Compose** | Container orchestration (v2 and v5) | [`docker-compose/SKILL.md`](docker-compose/SKILL.md) |
2524

2625
## How Skills Work
2726

2827
Skills use the Agent Skills progressive disclosure model:
2928

30-
1. **YAML frontmatter** (always loaded) `name` and `description` tell Claude
29+
1. **YAML frontmatter** (always loaded). `name` and `description` tell Claude
3130
when the skill is relevant.
32-
2. **`SKILL.md` body** (loaded on trigger) — core instructions and guidance.
33-
3. **Bundled files** (optional, loaded on demand) — deeper references, scripts,
31+
2. **`SKILL.md` body** (loaded on trigger). Core instructions and guidance.
32+
3. **Bundled files** (optional, loaded on demand). Deeper references, scripts,
3433
or templates referenced from `SKILL.md`.
3534

3635
The current skills intentionally keep only concise `SKILL.md` files. Add
@@ -58,8 +57,8 @@ Zip a skill folder and upload via **Settings → Capabilities → Skills**.
5857
### Claude API
5958

6059
Pass the skill via `container.skills` on the Messages API (requires the Code
61-
Execution Tool beta). See the
62-
[Skills API Quickstart](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills).
60+
Execution Tool beta). See
61+
[Using Agent Skills with the API](https://platform.claude.com/docs/en/build-with-claude/skills-guide).
6362

6463
## Skill Structure
6564

@@ -87,11 +86,29 @@ To add a new skill:
8786
2. Add a `SKILL.md` with valid YAML frontmatter (`name` matches the folder;
8887
`description` includes both *what it does* and *when to use it*, with
8988
specific trigger phrases).
90-
3. Keep `SKILL.md` focused; move deep reference material to `references/` and
89+
3. Cover the required sections: `Scope`, `Inspect First`, `Safety`, `Verify`,
90+
and `Update Checklist`. These keep each skill an operating guide rather
91+
than a reference article.
92+
4. Keep `SKILL.md` focused; move deep reference material to `references/` and
9193
executable helpers to `scripts/` within the skill folder.
92-
4. Update the table above.
94+
5. Update the table above.
95+
96+
Run the validator before opening a pull request:
97+
98+
```bash
99+
pip install pyyaml
100+
python3 .github/scripts/validate_skills.py
101+
```
102+
103+
It checks frontmatter validity, name and directory agreement, description
104+
length, required sections, file size, directory layout, relative links, and
105+
README coverage. A skill folder may contain only `SKILL.md` plus `references/`,
106+
`scripts/`, and `assets/`. CI runs the same script on every pull request,
107+
alongside a weekly link check.
108+
109+
For full authoring guidance, see Anthropic's
110+
[Complete Guide to Building Skills for Claude][guide] (PDF, ~33 pages) and the
111+
[Skill authoring best practices][best-practices] documentation.
93112

94-
See Anthropic's
95-
[Complete Guide to Building Skills for Claude](https://www.anthropic.com)
96-
(bundled as `The-Complete-Guide-to-Building-Skill-for-Claude.pdf` in this repo)
97-
for full authoring guidance.
113+
[guide]: https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf
114+
[best-practices]: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices

0 commit comments

Comments
 (0)