diff --git a/scripts/check-vale-style-sync.sh b/scripts/check-vale-style-sync.sh index f6ede61..49e6a9d 100755 --- a/scripts/check-vale-style-sync.sh +++ b/scripts/check-vale-style-sync.sh @@ -80,26 +80,45 @@ done # Prints the `files:` regex of every hook, in either manifest, whose entry is # $1's vale-wrap.sh. Records are delimited by their `- id:` line, so the check # does not depend on `entry:` preceding `files:` within a record. +# +# Cached per skill (in HOOK_REGEX_CACHE, populated lazily) because the final +# validation loop below probes agent-audit twice — once for its CC agent-file +# shape, once for its Copilot .agent.md shape — and both probes need the same +# regex set. Without the cache, that pair of calls would each re-parse both +# manifest files from scratch for no new information. HOOK_REGEX_CACHE_SEEN is +# a separate array so a skill with no matching hooks (empty result) is still +# recognized as already computed, rather than re-parsed every call. +declare -A HOOK_REGEX_CACHE=() +declare -A HOOK_REGEX_CACHE_SEEN=() hook_file_regexes() { - local skill="$1" manifest raw - for manifest in "$REPO_ROOT/.pre-commit-hooks.yaml" "$REPO_ROOT/.pre-commit-config.yaml"; do - [[ -f "$manifest" ]] || continue - awk -v skill="$skill" ' - function flush() { - if (entry ~ skill "/scripts/vale-wrap.sh" && files != "") print files - entry = ""; files = "" - } - /^[ \t]*-[ \t]*id:/ { flush() } - /^[ \t]*entry:/ { entry = $0 } - /^[ \t]*files:/ { files = $0; sub(/^[ \t]*files:[ \t]*/, "", files) } - END { flush() } - ' "$manifest" - done | while IFS= read -r raw; do - # Strip the surrounding YAML quotes; the regex itself never carries them. - raw="${raw%\'}"; raw="${raw#\'}" - raw="${raw%\"}"; raw="${raw#\"}" - printf '%s\n' "$raw" - done + local skill="$1" manifest raw result + if [[ -n "${HOOK_REGEX_CACHE_SEEN[$skill]:-}" ]]; then + printf '%s' "${HOOK_REGEX_CACHE[$skill]}" + return + fi + result="$( + for manifest in "$REPO_ROOT/.pre-commit-hooks.yaml" "$REPO_ROOT/.pre-commit-config.yaml"; do + [[ -f "$manifest" ]] || continue + awk -v skill="$skill" ' + function flush() { + if (entry ~ skill "/scripts/vale-wrap.sh" && files != "") print files + entry = ""; files = "" + } + /^[ \t]*-[ \t]*id:/ { flush() } + /^[ \t]*entry:/ { entry = $0 } + /^[ \t]*files:/ { files = $0; sub(/^[ \t]*files:[ \t]*/, "", files) } + END { flush() } + ' "$manifest" + done | while IFS= read -r raw; do + # Strip the surrounding YAML quotes; the regex itself never carries them. + raw="${raw%\'}"; raw="${raw#\'}" + raw="${raw%\"}"; raw="${raw#\"}" + printf '%s\n' "$raw" + done + )" + HOOK_REGEX_CACHE[$skill]="$result" + HOOK_REGEX_CACHE_SEEN[$skill]=1 + printf '%s' "$result" } # Asks vale — the thing that actually applies these globs — whether a config diff --git a/scripts/skill-size-check.sh b/scripts/skill-size-check.sh index a1cf3bc..2c555e6 100755 --- a/scripts/skill-size-check.sh +++ b/scripts/skill-size-check.sh @@ -35,6 +35,13 @@ set -euo pipefail # tokenization and does not replace one. Re-measure the corpus before treating # any of these numbers as still current. +# These constants are intentionally duplicated in +# skill-audit/scripts/validate.sh (Python) rather than shared from one file: +# this script is a standalone bash pre-commit hook, that one is an in-skill +# Python validator invoked in a different context (same rationale as +# vale-wrap.sh's per-plugin duplication — see its own header comment). +# tests/test-skill-size-check.sh asserts both files agree on these values, so +# drift between them fails CI rather than silently diverging. MAX_LINES=500 MAX_WORDS=2770 FAIL=0 @@ -42,16 +49,19 @@ FAIL=0 for f in "$@"; do [[ -f "$f" ]] || continue - # awk's NR counts the final line even without a trailing newline, matching - # Python's splitlines() semantics (used by skill-audit/scripts/validate.sh - # for its own line count) — `wc -l` undercounts by 1 in that case. - lines=$(awk 'END{print NR}' "$f") + # Single awk pass computes both line count and word count, avoiding a + # second read of the file. NR counts the final line even without a + # trailing newline, matching Python's splitlines() semantics (used by + # skill-audit/scripts/validate.sh for its own line count) — `wc -l` + # undercounts by 1 in that case. Word count uses awk's default + # whitespace-splitting NF, matching `wc -w` semantics. + read -r lines words <<< "$(awk '{w += NF} END{print NR, w+0}' "$f")" + if (( lines > MAX_LINES )); then echo "ERROR: $f has $lines lines, exceeding the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2 FAIL=1 fi - words=$(wc -w < "$f") if (( words > MAX_WORDS )); then echo "ERROR: $f has $words words (proxy for tokens), exceeding the $MAX_WORDS-word ceiling (~5,000 tokens, agentskills.io skill-authoring.md)" >&2 FAIL=1