Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions .github/scripts/check-portability.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

FORBIDDEN_PATTERNS = (
("Hermes tool name", re.compile(r"\bskill_(?:view|manage)\b", re.IGNORECASE)),
("Hermes config path", re.compile(r"~/\\.hermes(?:/|\b)", re.IGNORECASE)),
("Hermes config path", re.compile(r"~/\.hermes(?:/|\b)", re.IGNORECASE)),
(
"Hermes CLI command",
re.compile(
Expand Down Expand Up @@ -53,7 +53,54 @@ def scan_file(path: Path) -> list[tuple[Path, int, str, str]]:
return violations


def run_self_tests() -> int:
"""Run internal self-tests for the validation logic."""
test_cases = [
("skill_view(name)", True, "Hermes tool name"),
("SKILL_VIEW(name)", True, "Hermes tool name case insensitive"),
("from hermes_tools import foo", True, "Hermes Python import"),
("from HERMES_TOOLS import foo", True, "Hermes Python import case insensitive"),
("~/.hermes/config", True, "Hermes config path"),
("~/.hermes/", True, "Hermes config path with slash"),
("hermes skills install", True, "Hermes CLI command"),
("hermes config set", True, "Hermes CLI command"),
("Claude()", True, "Claude Code agent reference"),
("gemini skills", True, "Gemini CLI command"),
("codex run", True, "Codex CLI command"),
(".claude/config", True, "Platform-specific path"),
(".cursor/config", True, "Platform-specific path"),
("normal text", False, "Normal text"),
("skill_manage(action='create')", True, "Hermes tool name"),
]

for text, should_match, label in test_cases:
for pattern_label, pattern in FORBIDDEN_PATTERNS:
if pattern.search(text):
if not should_match:
print(f"FAIL: {label} matched unexpectedly: {text!r}")
return 1
break
else:
if should_match:
print(f"FAIL: {label} should have matched: {text!r}")
return 1

print("PASS: check-portability.py self-tests")
return 0


def main() -> int:
import argparse

parser = argparse.ArgumentParser(
description="Check for agent-specific references in portable skill files."
)
parser.add_argument("--self-test", action="store_true", help="Run internal self-tests")
args = parser.parse_args()

if args.self_test:
return run_self_tests()

violations: list[tuple[Path, int, str, str]] = []
for path in sorted(SKILLS_DIR.rglob("*.md")):
try:
Expand All @@ -73,4 +120,4 @@ def main() -> int:


if __name__ == "__main__":
sys.exit(main())
sys.exit(main())
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ jobs:
- name: Check cross-agent portability
run: python3 .github/scripts/check-portability.py

- name: Check version consistency
run: python3 scripts/check-version-consistency.py

- name: Validate CI policy
run: python3 scripts/validate-ci.py

Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ cff-version: 1.2.0
message: "If you use this work in your research, please cite it as below."
authors:
- family-names: "CodeSigils"
email: "toolsoftrade.web@gmail.com"
title: "Python Project Workflow"
version: "0.1.0"
license: MIT
type: software
repository-code: https://github.com/CodeSigils/python-project-workflow-skill
Expand Down
153 changes: 153 additions & 0 deletions scripts/check-version-consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Check local release versions and git tags for consistency."""

from __future__ import annotations

import re
import subprocess
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]

LOCAL_VERSION_SOURCES: dict[str, Path] = {
"SKILL.md": ROOT / "skills" / "python-project-workflow" / "SKILL.md",
"CITATION.cff": ROOT / "CITATION.cff",
}


def normalize_version(version: str) -> str:
return version.removeprefix("v")


def read_skill_version(path: Path) -> str | None:
"""Extract metadata version from SKILL.md frontmatter."""
try:
content = path.read_text(encoding="utf-8")
except OSError:
return None
frontmatter = content.split("---", 2)
if len(frontmatter) < 3:
return None
match = re.search(r'(?m)^version:\s*["\']?([^"\'#\s]+)', frontmatter[1])
return match.group(1) if match else None


def read_citation_version(path: Path) -> str | None:
"""Extract the version field from CITATION.cff."""
try:
content = path.read_text(encoding="utf-8")
except OSError:
return None
match = re.search(r'(?m)^version:\s*["\']?([^"\'#\s]+)', content)
return match.group(1) if match else None


def get_latest_tag() -> tuple[str | None, str | None]:
"""Return the latest v-prefixed tag and an optional query error."""
try:
result = subprocess.run(
["git", "tag", "--list", "v*", "--sort=-version:refname"],
capture_output=True,
text=True,
check=False,
)
except OSError as exc:
return None, str(exc)
if result.returncode != 0:
return None, result.stderr.strip() or f"git exited {result.returncode}"
tags = result.stdout.splitlines()
return (tags[0], None) if tags else (None, None)


def validate_versions(
local_versions: dict[str, str | None],
latest_tag: str | None,
tag_error: str | None,
) -> tuple[list[str], list[str]]:
"""Validate version consistency and return errors plus informational notes."""
errors: list[str] = []
notes: list[str] = []
comparable: dict[str, str] = {}

for source in LOCAL_VERSION_SOURCES:
version = local_versions.get(source)
if not version:
errors.append(f"Could not extract version from {source}")
else:
comparable[source] = normalize_version(version)

if tag_error:
notes.append(f"SKIP: could not query git tags: {tag_error}")
elif latest_tag:
comparable["latest tag"] = normalize_version(latest_tag)
else:
notes.append("SKIP: no v-prefixed git tags found")

if comparable and len(set(comparable.values())) > 1:
rendered = ", ".join(
f"{source}={version}" for source, version in comparable.items()
)
errors.append(f"Version drift: {rendered}")

return errors, notes


def run_self_tests() -> int:
"""Cover aligned, drifted, and no-tag states."""
aligned = {source: "0.1.0" for source in LOCAL_VERSION_SOURCES}

errors, notes = validate_versions(aligned, "v0.1.0", None)
assert errors == [] and notes == []

drifted = {**aligned, "CITATION.cff": "0.2.0"}
errors, _ = validate_versions(drifted, "v0.1.0", None)
assert any(error.startswith("Version drift:") for error in errors)

errors, notes = validate_versions(aligned, None, None)
assert errors == [] and notes == ["SKIP: no v-prefixed git tags found"]

errors, notes = validate_versions(aligned, None, "not a git repository")
assert errors == [] and notes == [
"SKIP: could not query git tags: not a git repository"
]

print("PASS: check-version-consistency.py self-tests")
return 0


def main() -> int:
import argparse

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()

if args.self_test:
return run_self_tests()

local_versions = {
"SKILL.md": read_skill_version(LOCAL_VERSION_SOURCES["SKILL.md"]),
"CITATION.cff": read_citation_version(LOCAL_VERSION_SOURCES["CITATION.cff"]),
}
latest_tag, tag_error = get_latest_tag()

for source, version in local_versions.items():
print(f"{source} version: {version or 'unreadable'}")
print(f"Latest tag: {latest_tag or 'none'}")

errors, notes = validate_versions(local_versions, latest_tag, tag_error)
for note in notes:
print(note)
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1

print("OK: all available version sources are consistent")
return 0


if __name__ == "__main__":
sys.exit(main())
1 change: 1 addition & 0 deletions scripts/validate-ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
SHA_PIN_RE = re.compile(r"^[^@\s]+@[0-9a-f]{40}$")
REQUIRED_VALIDATE_COMMANDS = (
"python3 .github/scripts/check-portability.py",
"python3 scripts/check-version-consistency.py",
"python3 scripts/validate-ci.py",
"python3 scripts/validate.py",
"python3 scripts/test-validate-ci.py",
Expand Down
2 changes: 2 additions & 0 deletions scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
fail(f"unsupported frontmatter fields: {sorted(extra)}")
if data.get("name") != "python-project-workflow":
fail("frontmatter name must be python-project-workflow")
if not data.get("version"):
fail("frontmatter must include a version field")
if len(data.get("description", "").split()) < 18:
fail("frontmatter description is too short to trigger reliably")
return data, body
Expand Down
1 change: 1 addition & 0 deletions skills/python-project-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
name: python-project-workflow
version: "0.1.0"
description: Set up, inspect, preserve, and verify Python projects across greenfield bootstrap, tooling configuration, CI, packaging, mature-repo preservation, and project-native verification. Use for Python project workflow tasks, not for pure code review; use py-review-skill or another dedicated review skill for review findings.
---

Expand Down