ci: validate SKILL.md frontmatter and README skill table (#2)

- Add .github/workflows/validate-skills.yml (runs on PRs + pushes to main)
- Add validator: every skills/*/SKILL.md needs valid YAML frontmatter
  with non-empty name + description; README table must match skills/ dirs
- Fix geo-query-finder/SKILL.md: add missing frontmatter
- Add geo-query-finder + stripe-dispute rows to README table
- Remove stale marketing-psychology row (no skill dir)
- Bump skill count to 65+ in README and USAGE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Quanlai Li
2026-06-10 23:44:54 -07:00
parent 53ad36b220
commit 52857dfa61
5 changed files with 117 additions and 3 deletions
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Validate skill frontmatter and README skill table.
Checks:
1. Every skills/*/SKILL.md has a YAML frontmatter block (delimited by ---).
2. The frontmatter parses as valid YAML and has non-empty `name` and `description`.
3. The README skill table lists exactly the skills present in skills/ (count + names).
Exits non-zero with a report if any check fails.
"""
import pathlib
import re
import sys
import yaml
ROOT = pathlib.Path(__file__).resolve().parents[2]
SKILLS_DIR = ROOT / "skills"
README = ROOT / "README.md"
# Matches README table rows like: | `skill-name` | description | ... |
TABLE_ROW = re.compile(r"^\|\s*`([a-z0-9-]+)`\s*\|")
def parse_frontmatter(text: str):
"""Return the parsed frontmatter dict, or raise ValueError."""
if not text.startswith("---"):
raise ValueError("missing YAML frontmatter (file must start with '---')")
parts = text.split("---", 2)
if len(parts) < 3:
raise ValueError("frontmatter block is not closed with a second '---'")
data = yaml.safe_load(parts[1])
if not isinstance(data, dict):
raise ValueError("frontmatter is not a YAML mapping")
return data
def main() -> int:
errors = []
skill_dirs = sorted(p.name for p in SKILLS_DIR.iterdir() if p.is_dir())
for name in skill_dirs:
skill_md = SKILLS_DIR / name / "SKILL.md"
if not skill_md.exists():
errors.append(f"{name}: missing SKILL.md")
continue
try:
data = parse_frontmatter(skill_md.read_text(encoding="utf-8"))
except (ValueError, yaml.YAMLError) as exc:
errors.append(f"{name}/SKILL.md: {exc}")
continue
for field in ("name", "description"):
value = data.get(field)
if not (isinstance(value, str) and value.strip()):
errors.append(f"{name}/SKILL.md: missing or empty `{field}` field")
# Reconcile the README skill table against the skills/ directory.
documented = {m.group(1) for line in README.read_text(encoding="utf-8").splitlines()
if (m := TABLE_ROW.match(line))}
actual = set(skill_dirs)
undocumented = sorted(actual - documented)
phantom = sorted(documented - actual)
if undocumented:
errors.append("README skill table is missing rows for: " + ", ".join(undocumented))
if phantom:
errors.append("README skill table has rows with no skills/ dir: " + ", ".join(phantom))
if len(documented) != len(actual):
errors.append(f"README skill count ({len(documented)}) != skills/ dir count ({len(actual)})")
if errors:
print("Skill validation FAILED:\n")
for e in errors:
print(f" - {e}")
return 1
print(f"All {len(actual)} skills valid; README table matches skills/ directory.")
return 0
if __name__ == "__main__":
sys.exit(main())
+20
View File
@@ -0,0 +1,20 @@
name: Validate Skills
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install PyYAML
run: pip install pyyaml
- name: Validate SKILL.md frontmatter and README table
run: python3 .github/scripts/validate_skills.py
+3 -2
View File
@@ -40,7 +40,7 @@ Most AI marketing tools charge **$50300/month** for a chat box that gives you
**Prerequisites:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated.
```bash
# Install all 63+ marketing skills
# Install all 65+ marketing skills
npx openclaudia install --all
# Or install specific skills
@@ -94,6 +94,7 @@ cp -r skills/seo-audit .claude/skills/ # project-level
| `schema-markup` | Generate and validate Schema.org structured data |
| `programmatic-seo` | Create SEO-optimized pages at scale |
| `ahrefs-research` | Ahrefs Python SDK for backlinks, keywords, domain ratings, and traffic |
| `geo-query-finder` | Find which ChatGPT search queries mention a brand (GEO visibility) |
### Content Writing
| Skill | Description |
@@ -149,7 +150,6 @@ cp -r skills/seo-audit .claude/skills/ # project-level
| Skill | Description |
|-------|-------------|
| `marketing-ideas` | 139 proven marketing ideas by category |
| `marketing-psychology` | 70+ psychological principles for marketing |
| `competitor-analysis` | Full competitor strategy breakdown |
| `pricing-strategy` | Pricing page and strategy optimization |
| `launch-strategy` | Product launch planning and execution |
@@ -184,6 +184,7 @@ cp -r skills/seo-audit .claude/skills/ # project-level
| `newsletter` | Newsletter growth, engagement, and monetization |
| `podcast-marketing` | Podcast production, growth, and promotion |
| `podcast-edit` | Edit podcast audio — trim, remove fillers, normalize loudness |
| `stripe-dispute` | Fight Stripe disputes and chargebacks with evidence + counter-dispute |
### CRM & Outreach (API-Powered)
| Skill | Description | API Required |
+1 -1
View File
@@ -1,6 +1,6 @@
# How to Use OpenClaudia
A practical guide to getting the most out of OpenClaudia's 62+ marketing skills in Claude Code.
A practical guide to getting the most out of OpenClaudia's 65+ marketing skills in Claude Code.
---
+10
View File
@@ -1,3 +1,13 @@
---
name: geo-query-finder
description: >
Find which ChatGPT search queries mention a given brand. Tests long-tail
queries against ChatGPT's web-search-enabled model and reports which ones
surface the brand. Use when the user asks to "find queries for [brand]",
"check GEO visibility", "which queries mention [brand]", "geo query finder",
"find AI mentions", or "test ChatGPT queries for [brand]".
---
# GEO Query Finder
Find which ChatGPT search queries mention a given brand. Tests long-tail queries against ChatGPT's web-search-enabled model and reports which ones surface the brand.