diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5934500 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint + +on: + pull_request: + push: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Validate harness skill + run: python scripts/validate_skills.py + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Markdown lint (scoped) + run: | + npx --yes markdownlint-cli@0.43.0 \ + 'README*.md' \ + 'docs/**/*.md' \ + 'skills/**/*.md' \ + 'CONTRIBUTING.md' \ + 'CHANGELOG.md' \ + '.github/**/*.md' \ + --ignore node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be8060..ffc4fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ## [Unreleased] ### Added +- `scripts/validate_skills.py` — SKILL frontmatter + reference link validation +- `.github/workflows/lint.yml` — CI: validate_skills + scoped markdownlint - 신규 에이전트/스킬 생성 전 중복 검토 단계 (Phase 3-0, Phase 4-0) - `references/agent-design-patterns.md` "에이전트 재사용 설계" 섹션 - `references/skill-writing-guide.md` §9 "스킬 재사용 설계" diff --git a/scripts/validate_skills.py b/scripts/validate_skills.py new file mode 100644 index 0000000..cd539cd --- /dev/null +++ b/scripts/validate_skills.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Validate harness skill metadata and reference links.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SKILL = ROOT / "skills" / "harness" / "SKILL.md" +REFS = ROOT / "skills" / "harness" / "references" + + +def validate_frontmatter(text: str) -> list[str]: + errors: list[str] = [] + if not text.startswith("---"): + return ["SKILL.md missing YAML frontmatter"] + match = re.match(r"---\n(.*?)\n---", text, re.DOTALL) + if not match: + return ["SKILL.md has invalid frontmatter block"] + frontmatter = match.group(1) + if "name:" not in frontmatter: + errors.append("frontmatter missing name") + if "description:" not in frontmatter: + errors.append("frontmatter missing description") + return errors + + +def validate_references(text: str) -> list[str]: + errors: list[str] = [] + for ref in re.findall(r"`(references/[^`]+)`", text): + path = ROOT / "skills" / "harness" / ref + if not path.exists(): + errors.append(f"broken reference link: {ref}") + return errors + + +def main() -> int: + errors: list[str] = [] + if not SKILL.exists(): + errors.append(f"missing {SKILL}") + else: + text = SKILL.read_text(encoding="utf-8") + errors.extend(validate_frontmatter(text)) + errors.extend(validate_references(text)) + if len(text.splitlines()) > 520: + errors.append("SKILL.md exceeds 520 lines (target <500)") + + if REFS.is_dir(): + for md in REFS.glob("*.md"): + if len(md.read_text(encoding="utf-8").splitlines()) > 650: + errors.append(f"{md.name} exceeds 650 lines") + + if errors: + for err in errors: + print(f"ERROR: {err}", file=sys.stderr) + return 1 + + print("validate_skills: OK") + return 0 + + +if __name__ == "__main__": + sys.exit(main())