mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
092b99dad9
## What
Adds `scripts/skill-eval.sh` — a thin gate that runs one skill's
`SKILL.md` through Jeff Emanuel's `ms` (meta_skill) `lint` + `validate`.
The wrapper decides the gate purely on whether a **blocking** `rule_id`
appears (ms's own severity/exit is not trusted):
- Blocking: `no-secrets`, `no-injection`, `safe-paths`,
`required-metadata`, `no-cycle`, `valid-version` → non-zero exit.
- Everything else → annotated only (`::warning::`), never fails.
- `ms` not on PATH (or `MS_BIN` missing) → **loud `::error::` hard-fail
(exit 3)**, never skip-and-pass — a silent skip would recreate the
no-evaluation gap this gate closes.
Planted `skills/_fixtures/{bad,good}-skill/SKILL.md` fixtures +
`tests/scripts/skill-eval.bats` (10 tests) exercise the
bad/good/ms-absent contracts.
## Fixture scoping
`skills/_fixtures/` is non-skill scaffolding (planted fixtures). The
skill-enumerating gates/generators are taught to skip `skills/_*` so the
fixtures never inflate the skill count (stays 82) or fail registry /
schema / wiring / runtime / count gates. Touched enumerators:
`generate-registry`, `validate-skill-schema`, `sync-skill-counts`,
`validate-skill-count`, `generate-skill-catalog`,
`generate-context-map`, `validate-manifests`, `check-wiring-closure`,
`validate-headless-runtime-skills`, `lib/sku_catalog.py`,
`check-alias-collisions`, `run-all`, `test-runtime-opencode-smoke`,
`release-smoke-test`, `smoke-test`, `validate-skill`.
Does **not** touch `validate.yml` — wiring skill-eval.sh into CI is a
separate bead.
Epic: ag-czzf
## Verification
- `bats tests/scripts/skill-eval.bats` → 10/10 pass (ms 0.1.2 on PATH)
- `shellcheck scripts/skill-eval.sh` → clean
- `check-registry-drift`, `validate-skill-schema`, `sync-skill-counts`,
`run-all`, opencode-smoke → all PASS with fixtures present, skill count
= 82 (fixtures excluded)
Closes-scenario: ag-yzoz#skill-eval
Bounded-context: BC2-Validation
Evidence: scripts/skill-eval.sh
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
213 lines
6.4 KiB
Bash
Executable File
213 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
SCOPE="auto"
|
|
SKILLS_ROOT="$ROOT/skills-codex"
|
|
MANIFEST_FILE="$SKILLS_ROOT/.agentops-manifest.json"
|
|
MARKER_FILE_NAME=".agentops-generated.json"
|
|
MANIFEST_VALIDATOR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/validate-codex-generated-manifest.sh"
|
|
AUDIT_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/audit-codex-parity.sh"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: bash scripts/validate-codex-generated-artifacts.sh [repo-root] [--scope auto|upstream|staged|worktree|head]
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--scope)
|
|
SCOPE="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--*)
|
|
echo "Unknown arg: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
ROOT="$1"
|
|
if [[ "$ROOT" != /* ]]; then
|
|
ROOT="$(cd "$ROOT" && pwd)"
|
|
fi
|
|
SKILLS_ROOT="$ROOT/skills-codex"
|
|
MANIFEST_FILE="$SKILLS_ROOT/.agentops-manifest.json"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
case "$SCOPE" in
|
|
auto|upstream|staged|worktree|head) ;;
|
|
*)
|
|
echo "Invalid --scope: $SCOPE" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
failures=0
|
|
warnings=0
|
|
|
|
fail() {
|
|
echo "FAIL: $1" >&2
|
|
failures=$((failures + 1))
|
|
}
|
|
|
|
warn() {
|
|
echo "WARN: $1" >&2
|
|
warnings=$((warnings + 1))
|
|
}
|
|
|
|
collect_changed_files() {
|
|
local scope="$1"
|
|
local ahead_files=""
|
|
|
|
if ! git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
case "$scope" in
|
|
upstream)
|
|
if git -C "$ROOT" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
|
|
git -C "$ROOT" diff --name-only '@{upstream}...HEAD' 2>/dev/null || true
|
|
fi
|
|
;;
|
|
staged)
|
|
git -C "$ROOT" diff --name-only --cached 2>/dev/null || true
|
|
;;
|
|
worktree)
|
|
git -C "$ROOT" diff --name-only --cached 2>/dev/null || true
|
|
git -C "$ROOT" diff --name-only 2>/dev/null || true
|
|
git -C "$ROOT" ls-files --others --exclude-standard 2>/dev/null || true
|
|
;;
|
|
head)
|
|
git -C "$ROOT" show --name-only --pretty=format: HEAD 2>/dev/null || true
|
|
;;
|
|
auto)
|
|
if git -C "$ROOT" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
|
|
ahead_files="$(git -C "$ROOT" diff --name-only '@{upstream}...HEAD' 2>/dev/null || true)"
|
|
if [[ -n "$ahead_files" ]]; then
|
|
printf '%s\n' "$ahead_files"
|
|
return 0
|
|
fi
|
|
fi
|
|
git -C "$ROOT" diff --name-only --cached 2>/dev/null || true
|
|
git -C "$ROOT" diff --name-only 2>/dev/null || true
|
|
git -C "$ROOT" ls-files --others --exclude-standard 2>/dev/null || true
|
|
git -C "$ROOT" show --name-only --pretty=format: HEAD 2>/dev/null || true
|
|
;;
|
|
esac
|
|
}
|
|
|
|
echo "=== Codex artifact metadata validation ==="
|
|
|
|
[[ -d "$SKILLS_ROOT" ]] || {
|
|
echo "Missing skills-codex root: $SKILLS_ROOT" >&2
|
|
exit 1
|
|
}
|
|
[[ -f "$MANIFEST_FILE" ]] || {
|
|
echo "Missing Codex artifact manifest: $MANIFEST_FILE" >&2
|
|
exit 1
|
|
}
|
|
if [[ -x "$MANIFEST_VALIDATOR" ]]; then
|
|
bash "$MANIFEST_VALIDATOR" "$SKILLS_ROOT" >/dev/null
|
|
fi
|
|
|
|
while IFS= read -r skill_dir; do
|
|
[[ -f "$skill_dir/SKILL.md" ]] || continue
|
|
[[ -f "$skill_dir/$MARKER_FILE_NAME" ]] || fail "missing Codex artifact marker: ${skill_dir#"$ROOT"/}/$MARKER_FILE_NAME"
|
|
if grep -qE "^description:[[:space:]]*['\"]?[>|]['\"]?[[:space:]]*$" "$skill_dir/SKILL.md"; then
|
|
fail "malformed generated description frontmatter: ${skill_dir#"$ROOT"/}/SKILL.md"
|
|
fi
|
|
done < <(find "$SKILLS_ROOT" -mindepth 1 -maxdepth 1 -type d | LC_ALL=C sort)
|
|
|
|
# --- Frontmatter completeness check ---
|
|
for skill_md in "$SKILLS_ROOT"/*/SKILL.md; do
|
|
[[ -f "$skill_md" ]] || continue
|
|
skill_name=$(basename "$(dirname "$skill_md")")
|
|
frontmatter_fields=""
|
|
|
|
# Extract only the leading frontmatter block.
|
|
frontmatter=$(awk 'NR==1 && /^---$/{in_fm=1; print; next} in_fm && /^---$/{print; exit} in_fm{print}' "$skill_md")
|
|
frontmatter_fields="$(printf '%s\n' "$frontmatter" | grep -oE '^[a-z_-]+:' | sed 's/:$//' || true)"
|
|
|
|
if ! echo "$frontmatter" | grep -q '^name:'; then
|
|
fail "$skill_name missing 'name' in frontmatter"
|
|
fi
|
|
if ! echo "$frontmatter" | grep -q '^description:'; then
|
|
fail "$skill_name missing 'description' in frontmatter"
|
|
fi
|
|
extra_fields="$(printf '%s\n' "$frontmatter_fields" | grep -vE '^(name|description)$' || true)"
|
|
if [[ -n "$extra_fields" ]]; then
|
|
fail "$skill_name has non-Codex frontmatter fields: $(printf '%s' "$extra_fields" | tr '\n' ',' | sed 's/,$//')"
|
|
fi
|
|
done
|
|
|
|
# --- Wrong-directory cross-reference check ---
|
|
for skill_md in "$SKILLS_ROOT"/*/SKILL.md; do
|
|
[[ -f "$skill_md" ]] || continue
|
|
skill_name=$(basename "$(dirname "$skill_md")")
|
|
# Ignore code blocks by checking only non-fenced lines
|
|
if grep -v '^\s*```' "$skill_md" | grep -v '^\s*`' | grep -qE '\]\(skills/' ; then
|
|
warn "$skill_name contains ](skills/ cross-ref (should use relative paths)"
|
|
fi
|
|
done
|
|
|
|
mapfile -t changed_files < <(collect_changed_files "$SCOPE" | sed '/^[[:space:]]*$/d' | sort -u)
|
|
|
|
if [[ "${#changed_files[@]}" -gt 0 ]]; then
|
|
declare -A changed_source_skills=()
|
|
declare -A changed_codex_skills=()
|
|
|
|
for changed_file in "${changed_files[@]}"; do
|
|
case "$changed_file" in
|
|
skills/_*/*)
|
|
# Leading-underscore scaffolding under skills/ is not real skill
|
|
# source and has no Codex twin under skills-codex/.
|
|
;;
|
|
skills/*/*)
|
|
skill_name="${changed_file#skills/}"
|
|
skill_name="${skill_name%%/*}"
|
|
changed_source_skills["$skill_name"]=1
|
|
;;
|
|
skills-codex/*/*)
|
|
skill_name="${changed_file#skills-codex/}"
|
|
skill_name="${skill_name%%/*}"
|
|
changed_codex_skills["$skill_name"]=1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
for skill_name in "${!changed_source_skills[@]}"; do
|
|
if [[ -z "${changed_codex_skills[$skill_name]+x}" ]]; then
|
|
fail "source skill changed without matching checked-in Codex update: skills/$skill_name -> skills-codex/$skill_name"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# --- Invoke codex parity audit ---
|
|
if [[ -x "$AUDIT_SCRIPT" ]]; then
|
|
echo "--- Running codex parity audit ---"
|
|
if ! bash "$AUDIT_SCRIPT"; then
|
|
fail "Codex parity audit failed"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$warnings" -gt 0 ]]; then
|
|
echo "Codex artifact metadata validation: $warnings warning(s)." >&2
|
|
fi
|
|
|
|
if [[ "$failures" -gt 0 ]]; then
|
|
echo "Repair flow: bash scripts/refresh-codex-artifacts.sh --scope $SCOPE" >&2
|
|
echo "Codex artifact metadata validation FAILED ($failures finding(s))." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Codex artifact metadata validation passed."
|
|
exit 0
|