Skip to content

Add fastcrw-research skill for recall-first arXiv paper retrieval #52

Add fastcrw-research skill for recall-first arXiv paper retrieval

Add fastcrw-research skill for recall-first arXiv paper retrieval #52

name: Skill Spec Validation
on:
pull_request:
paths:
- "skills/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/skill-spec-validation.yml"
push:
branches:
- main
paths:
- "skills/**"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: skill-spec-validation-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
name: Validate skills against the Agent Skills spec
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v8.0.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
python-version: "3.13"
- name: Install dependencies
run: uv sync --python 3.13
# The reference validator from https://agentskills.io/specification. It checks the
# closed set of allowed frontmatter fields, name rules (incl. directory match),
# description/compatibility length limits, and parses frontmatter with strictyaml
# -- which rejects JSON-style flow mappings such as `metadata: {"version": "1.0"}`.
- name: skills-ref validate
run: |
set -uo pipefail
fail=0
for d in skills/*/; do
if ! out=$(uv run skills-ref validate "$d" 2>&1); then
fail=1
echo "::error file=${d}SKILL.md::$(echo "$out" | tail -n +2 | tr '\n' ' ')"
echo "FAIL $d"
echo "$out" | sed 's/^/ /'
fi
done
echo "Validated $(ls -d skills/*/ | wc -l) skills."
exit $fail
# Rules the reference validator does not enforce: this repo's metadata.version
# requirement (see AGENTS.md), plus spec constraints skills-ref accepts but the
# spec text requires -- allowed-tools must be a space-separated string, and
# metadata values must be strings apart from the host-manifest blocks that have
# to stay nested objects (see NESTED_OK below).
- name: Repo and spec rules skills-ref does not check
run: |
uv run --with pyyaml python - <<'PY'
import re
import sys
from pathlib import Path
import yaml
# Host manifest blocks that must stay nested mappings. OpenClaw's
# resolveOpenClawManifestBlock() requires `typeof candidate === "object"`, so
# encoding these as JSON strings silently disables its gating and credential
# injection. Nested mappings still pass `skills-ref validate`.
NESTED_OK = {"openclaw", "hermes"}
# Requires the closing delimiter on its own line. A naive split("---") would
# happily re-split at a `---` accidentally glued to the last frontmatter value.
FM_RE = re.compile(r"\A---\n(.*?)\n---\n", re.S)
errors, warnings = [], []
for d in sorted(Path("skills").iterdir()):
if not d.is_dir():
continue
md = d / "SKILL.md"
if not md.exists():
errors.append(f"{d}: missing SKILL.md")
continue
text = md.read_text()
m_fm = FM_RE.match(text)
if not m_fm:
errors.append(
f"{md}: frontmatter must open with `---` and close with `---` "
f"on its own line"
)
continue
fm = yaml.safe_load(m_fm.group(1))
at = fm.get("allowed-tools")
if at is not None:
if not isinstance(at, str):
errors.append(
f"{md}: allowed-tools must be a space-separated string, "
f"got {type(at).__name__}"
)
elif "," in at:
errors.append(
f"{md}: allowed-tools must be space-separated, not "
f"comma-separated: {at!r}"
)
m = fm.get("metadata")
if not isinstance(m, dict):
errors.append(f"{md}: missing a `metadata` mapping (see AGENTS.md)")
else:
if "version" not in m:
errors.append(f"{md}: metadata.version is required (see AGENTS.md)")
for k, v in m.items():
if k in NESTED_OK:
if not isinstance(v, dict):
errors.append(
f"{md}: metadata.{k} must stay a nested mapping, got "
f"{type(v).__name__} -- a JSON string silently disables "
f"host gating and credential injection"
)
continue
if isinstance(v, str):
continue
errors.append(
f"{md}: metadata.{k} must be a string, got {type(v).__name__} "
f"-- quote it (versions and dates especially)"
)
lines = text.count("\n") + 1
if lines > 500:
warnings.append(f"{md}: {lines} lines; the spec recommends under 500")
for w in warnings:
print(f"::warning file={w.split(':')[0]}::{w}")
for e in errors:
print(f"::error file={e.split(':')[0]}::{e}")
print(f"FAIL {e}")
print(f"\n{len(errors)} error(s), {len(warnings)} warning(s).")
sys.exit(1 if errors else 0)
PY