Files
boshu2__agentops/scripts/sync-skill-counts.sh
T
Bo 092b99dad9 feat(skills): add skill-eval.sh ms-backed quality gate (ag-yzoz #skill-eval) (#726)
## 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>
2026-06-04 16:38:57 -04:00

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