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>
264 lines
9.2 KiB
Bash
Executable File
264 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# sync-skill-counts.sh — Single-source-of-truth skill count updater.
|
|
# Reads actual counts from disk + SKILL-TIERS.md, patches all doc files.
|
|
# Run after adding/removing a skill to keep all references in sync.
|
|
#
|
|
# Usage: scripts/sync-skill-counts.sh [--check]
|
|
# --check Dry-run: report mismatches without modifying files (exit 1 if any)
|
|
set -euo pipefail
|
|
|
|
export LC_ALL=C
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
CHECK_ONLY=false
|
|
changes=0
|
|
errors=0
|
|
|
|
if [[ "${1:-}" == "--check" ]]; then
|
|
CHECK_ONLY=true
|
|
elif [[ $# -gt 0 ]]; then
|
|
echo "ERROR: unknown argument '$1'"
|
|
echo "Usage: scripts/sync-skill-counts.sh [--check]"
|
|
exit 2
|
|
fi
|
|
|
|
# --- Derive truth from disk ---
|
|
|
|
# `-not -name '_*'` excludes non-skill scaffolding dirs (e.g. skills/_fixtures/,
|
|
# planted test fixtures) from the skill count — they are not real skills.
|
|
TOTAL=$(find "$REPO_ROOT/skills" -mindepth 1 -maxdepth 1 -type d -not -name '.*' -not -name '_*' | wc -l | tr -d ' ')
|
|
CODEX_TOTAL=$(find "$REPO_ROOT/skills-codex" -mindepth 1 -maxdepth 1 -type d -not -name '.*' | wc -l | tr -d ' ')
|
|
CODEX_OVERRIDES=$(find "$REPO_ROOT/skills-codex-overrides" -mindepth 1 -maxdepth 1 -type d -not -name '.*' | wc -l | tr -d ' ')
|
|
HOOK_EVENT_SECTIONS=$(jq -r '.hooks | length' "$REPO_ROOT/hooks/hooks.json" 2>/dev/null || echo 0)
|
|
|
|
USER_FACING=$(sed -n '/^### User-Facing Skills/,/^### Internal Skills/p' "$REPO_ROOT/skills/SKILL-TIERS.md" \
|
|
| grep -c '^| \*\*')
|
|
|
|
INTERNAL_ROWS=$(sed -n '/^### Internal Skills/,/^---$/p' "$REPO_ROOT/skills/SKILL-TIERS.md" \
|
|
| grep -c '^| ')
|
|
INTERNAL=$((INTERNAL_ROWS - 1)) # subtract header row
|
|
|
|
echo "Skill counts from disk:"
|
|
echo " Total: $TOTAL"
|
|
echo " User-facing: $USER_FACING"
|
|
echo " Internal: $INTERNAL"
|
|
echo " Codex: $CODEX_TOTAL"
|
|
echo " Overrides: $CODEX_OVERRIDES"
|
|
echo " Hook events: $HOOK_EVENT_SECTIONS"
|
|
echo ""
|
|
|
|
if [[ "$INTERNAL_ROWS" -lt 1 ]]; then
|
|
echo "ERROR: SKILL-TIERS.md internal table header row not found"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ $((USER_FACING + INTERNAL)) -ne "$TOTAL" ]]; then
|
|
echo "ERROR: SKILL-TIERS.md tables ($((USER_FACING + INTERNAL))) != directories ($TOTAL)"
|
|
echo "Fix SKILL-TIERS.md first — add/remove the skill row, then re-run."
|
|
exit 1
|
|
fi
|
|
|
|
# --- Define all patch targets ---
|
|
|
|
patch_file() {
|
|
local file="$1"
|
|
local match_regex="$2"
|
|
local sed_expr="$3"
|
|
local desc="$4"
|
|
local match_count
|
|
local tmp_file
|
|
|
|
if [[ ! -f "$file" ]]; then
|
|
echo "ERROR: $desc file not found: $file"
|
|
errors=$((errors + 1))
|
|
return
|
|
fi
|
|
|
|
match_count=$(grep -E -c "$match_regex" "$file" || true)
|
|
if [[ "$match_count" -eq 0 ]]; then
|
|
echo "ERROR: $desc pattern not found (fail-closed)"
|
|
echo " file: $file"
|
|
echo " match: $match_regex"
|
|
errors=$((errors + 1))
|
|
return
|
|
fi
|
|
if [[ "$match_count" -gt 1 ]]; then
|
|
echo "ERROR: $desc pattern matched $match_count lines (expected exactly 1)"
|
|
echo " file: $file"
|
|
echo " match: $match_regex"
|
|
errors=$((errors + 1))
|
|
return
|
|
fi
|
|
|
|
tmp_file=$(mktemp)
|
|
sed -E "$sed_expr" "$file" > "$tmp_file"
|
|
|
|
if $CHECK_ONLY; then
|
|
if cmp -s "$file" "$tmp_file"; then
|
|
echo "OK: $desc"
|
|
else
|
|
echo "DRIFT: $desc"
|
|
changes=$((changes + 1))
|
|
fi
|
|
rm -f "$tmp_file"
|
|
return
|
|
fi
|
|
|
|
if cmp -s "$file" "$tmp_file"; then
|
|
echo "OK: $desc"
|
|
rm -f "$tmp_file"
|
|
return
|
|
fi
|
|
|
|
mv "$tmp_file" "$file"
|
|
echo "UPDATED: $desc"
|
|
changes=$((changes + 1))
|
|
}
|
|
|
|
check_file_pattern() {
|
|
local file="$1"
|
|
local match_regex="$2"
|
|
local desc="$3"
|
|
local match_count
|
|
|
|
if [[ ! -f "$file" ]]; then
|
|
echo "ERROR: $desc file not found: $file"
|
|
errors=$((errors + 1))
|
|
return
|
|
fi
|
|
|
|
match_count=$(grep -E -c "$match_regex" "$file" || true)
|
|
if [[ "$match_count" -eq 0 ]]; then
|
|
echo "ERROR: $desc pattern not found (fail-closed)"
|
|
echo " file: $file"
|
|
echo " match: $match_regex"
|
|
errors=$((errors + 1))
|
|
return
|
|
fi
|
|
|
|
if [[ "$match_count" -gt 1 ]]; then
|
|
echo "ERROR: $desc pattern matched $match_count lines (expected exactly 1)"
|
|
echo " file: $file"
|
|
echo " match: $match_regex"
|
|
errors=$((errors + 1))
|
|
return
|
|
fi
|
|
|
|
echo "OK: $desc"
|
|
}
|
|
|
|
# SKILL-TIERS.md header counts
|
|
patch_file "$REPO_ROOT/skills/SKILL-TIERS.md" \
|
|
'^### User-Facing Skills \([0-9]+\)' \
|
|
"s|^### User-Facing Skills \\([0-9]+\\)|### User-Facing Skills (${USER_FACING})|" \
|
|
"SKILL-TIERS.md user-facing header"
|
|
|
|
patch_file "$REPO_ROOT/skills/SKILL-TIERS.md" \
|
|
'^### Internal Skills \([0-9]+\)' \
|
|
"s|^### Internal Skills \\([0-9]+\\)|### Internal Skills (${INTERNAL})|" \
|
|
"SKILL-TIERS.md internal header"
|
|
|
|
# docs/SKILLS.md: "all N AgentOps skills (M user-facing + K internal)"
|
|
patch_file "$REPO_ROOT/docs/SKILLS.md" \
|
|
'^Complete reference for all [0-9]+ AgentOps skills \([0-9]+ user-facing [+] [0-9]+ internal\)\.$' \
|
|
"s|^Complete reference for all [0-9]+ AgentOps skills \\([0-9]+ user-facing [+] [0-9]+ internal\\)\\.$|Complete reference for all ${TOTAL} AgentOps skills (${USER_FACING} user-facing + ${INTERNAL} internal).|" \
|
|
"docs/SKILLS.md header"
|
|
|
|
# docs/ARCHITECTURE.md: "skills/ # N skills (M user-facing, K internal)"
|
|
patch_file "$REPO_ROOT/docs/ARCHITECTURE.md" \
|
|
'skills/[[:space:]]+# [0-9]+ skills \([0-9]+ user-facing, [0-9]+ internal\)$' \
|
|
"s|(skills/[[:space:]]+# )[0-9]+ skills \\([0-9]+ user-facing, [0-9]+ internal\\)$|\\1${TOTAL} skills (${USER_FACING} user-facing, ${INTERNAL} internal)|" \
|
|
"docs/ARCHITECTURE.md skills tree"
|
|
|
|
# PRODUCT.md current count surfaces.
|
|
patch_file "$REPO_ROOT/PRODUCT.md" \
|
|
'Skills system — [0-9]+ skills,' \
|
|
"s|(Skills system — )[0-9]+( skills,)|\\1${TOTAL}\\2|" \
|
|
"PRODUCT.md convergence skill count"
|
|
|
|
patch_file "$REPO_ROOT/PRODUCT.md" \
|
|
'[0-9]+ skills — reusable context packages' \
|
|
"s|[0-9]+ skills — reusable context packages|${TOTAL} skills — reusable context packages|" \
|
|
"PRODUCT.md context compiler skill count"
|
|
|
|
check_file_pattern "$REPO_ROOT/PRODUCT.md" \
|
|
'Runtime hooks are not an AgentOps default' \
|
|
"PRODUCT.md hookless posture"
|
|
|
|
patch_file "$REPO_ROOT/PRODUCT.md" \
|
|
'Distribution/runtime reach: [0-9]+ shared skills, [0-9]+ checked-in Codex artifacts, and [0-9]+ Codex overrides' \
|
|
"s|Distribution/runtime reach: [0-9]+ shared skills, [0-9]+ checked-in Codex artifacts, and [0-9]+ Codex overrides|Distribution/runtime reach: ${TOTAL} shared skills, ${CODEX_TOTAL} checked-in Codex artifacts, and ${CODEX_OVERRIDES} Codex overrides|" \
|
|
"PRODUCT.md distribution/runtime reach"
|
|
|
|
# docs/index.md: full-catalog link label "All N skills" (links to the generated
|
|
# catalog, which lists every checked-in skill, so this is the TOTAL).
|
|
patch_file "$REPO_ROOT/docs/index.md" \
|
|
'\*\*All [0-9]+ skills\*\*\]\(skills/catalog\.md\)' \
|
|
"s|\\*\\*All [0-9]+ skills\\*\\*\\]\\(skills/catalog\\.md\\)|**All ${TOTAL} skills**](skills/catalog.md)|" \
|
|
"docs/index.md catalog link count"
|
|
|
|
# docs/documentation-index.md: skill-domain-map entry "All N checked-in skills".
|
|
patch_file "$REPO_ROOT/docs/documentation-index.md" \
|
|
'All [0-9]+ checked-in skills mapped' \
|
|
"s|All [0-9]+ checked-in skills mapped|All ${TOTAL} checked-in skills mapped|" \
|
|
"docs/documentation-index.md domain-map skill count"
|
|
|
|
# docs/GLOSSARY.md: "ships N shared skills" (TOTAL — every checked-in skill).
|
|
patch_file "$REPO_ROOT/docs/GLOSSARY.md" \
|
|
'ships [0-9]+ shared skills' \
|
|
"s|ships [0-9]+ shared skills|ships ${TOTAL} shared skills|" \
|
|
"docs/GLOSSARY.md shared-skills count"
|
|
|
|
# --- ASCII-diagram skill-count guard (ag-c4wn) ---
|
|
# docs/agentops-system-map.md and docs/agentops-brief.md hardcode the skill
|
|
# count inside box-drawing borders whose column widths are alignment-sensitive.
|
|
# patch_file deliberately does NOT rewrite them — a digit-width change (e.g.
|
|
# 99 -> 100) would silently break the box. This is the "tolerant check" half:
|
|
# VERIFY each "<N> [shared ]skills" token equals TOTAL and fail on drift so a
|
|
# human re-pads the diagram by hand. Closes the drift class auto-patch leaves open.
|
|
DIAGRAM_FILES=(
|
|
"docs/agentops-system-map.md"
|
|
"docs/agentops-brief.md"
|
|
)
|
|
for rel in "${DIAGRAM_FILES[@]}"; do
|
|
f="$REPO_ROOT/$rel"
|
|
[[ -f "$f" ]] || continue
|
|
while IFS= read -r found; do
|
|
[[ -n "$found" ]] || continue
|
|
if [[ "$found" != "$TOTAL" ]]; then
|
|
echo "ERROR: $rel ASCII diagram hardcodes skill count $found, expected $TOTAL (re-pad the box-drawing diagram by hand)"
|
|
errors=$((errors + 1))
|
|
fi
|
|
done < <(grep -oiE '[0-9]+ +(shared )?skills\b' "$f" | grep -oE '^[0-9]+')
|
|
done
|
|
|
|
echo ""
|
|
|
|
if [[ "$errors" -gt 0 ]]; then
|
|
echo "FAIL: $errors pattern enforcement error(s) found"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Verify with existing validator ---
|
|
if ! $CHECK_ONLY; then
|
|
echo "=== Verifying with validate-skill-count.sh ==="
|
|
if bash "$REPO_ROOT/tests/docs/validate-skill-count.sh" > /dev/null 2>&1; then
|
|
echo "PASS: All counts verified consistent"
|
|
else
|
|
echo "FAIL: Counts still inconsistent after patching — run tests/docs/validate-skill-count.sh"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if [[ "$changes" -gt 0 ]]; then
|
|
if $CHECK_ONLY; then
|
|
echo "DRIFT: $changes file(s) have stale skill counts. Run: scripts/sync-skill-counts.sh"
|
|
exit 1
|
|
else
|
|
echo "DONE: $changes file(s) updated. Counts synced to $TOTAL total ($USER_FACING user-facing, $INTERNAL internal)."
|
|
fi
|
|
else
|
|
echo "DONE: All counts already in sync."
|
|
fi
|