Files
vnidrop/.codex/skills/compose-skill/scripts/validate.sh
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

2205 lines
85 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# ╔══════════════════════════════════════════════════════════════════╗
# ║ Agent Skill Scanner v4 ║
# ║ Validates skill packages against the agentskills.io spec ║
# ║ and best practices. Runs locally and in CI (GitHub Actions). ║
# ║ ║
# ║ Spec: https://agentskills.io/specification ║
# ║ Guide: https://agentskills.io/skill-creation ║
# ║ ║
# ║ Token Estimation: ║
# ║ - Uses character/4 approximation (industry standard) ║
# ║ - Accurate within ~10% for English text ║
# ║ - Based on OpenAI tiktoken cl100k_base encoding ║
# ║ ║
# ║ Quality Score: 0-100 across 5 dimensions ║
# ║ - Description Quality (30) · Spec Compliance (20) ║
# ║ - Instruction Clarity (25) · Progressive Disclosure (15) ║
# ║ - Security (10) ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# Usage:
# ./scripts/validate.sh # full scan
# ./scripts/validate.sh --help # show usage
#
# Environment:
# CI=true — emits GitHub Actions annotations (auto-detected)
# NO_COLOR=1 — disable colored output
SCAN_START=$SECONDS
ERRORS=0
WARNINGS=0
INFO_COUNT=0
CHECKS_RUN=0
TOTAL_CHECKS=15
CURRENT_SECTION=""
CI="${CI:-false}"
NO_COLOR="${NO_COLOR:-0}"
OUTPUT_MODE="terminal"
SKILL_FILE="SKILL.md"
SKILL_DIR="$(basename "$(pwd)")"
FINDINGS_FILE=$(mktemp)
trap 'rm -f "$FINDINGS_FILE"' EXIT
# ── Spec limits (agentskills.io/specification) ─────────────────────
NAME_MAX_LEN=64
DESC_MAX_LEN=1024
DESC_MIN_USEFUL=30
BODY_MAX_LINES=500
TOKEN_BUDGET=5000
COMPAT_MAX_LEN=500
# ── Token budget zones ────────────────────────────────────────────
TOKEN_SAFE_ZONE=3500
TOKEN_WARN_ZONE=5000
TOKEN_DANGER_ZONE=8000
REF_TOKEN_SAFE=2000
REF_TOKEN_WARN=3500
REF_TOKEN_DANGER=4500
TOTAL_TOKEN_SAFE=50000
TOTAL_TOKEN_WARN=100000
TOTAL_TOKEN_DANGER=200000
# ── Score trackers (populated during checks, consumed by scoring) ──
_S_HAS_NAME=false
_S_NAME_OK=false
_S_HAS_DESC=false
_S_DESC_LEN=0
_S_DESC_WHAT=false
_S_DESC_WHEN=false
_S_DESC_FIRST_PERSON=false
_S_DESC_GENERIC=false
_S_DESC_NEGATIVES=false
_S_CODE_BLOCKS=0
_S_HEADINGS=0
_S_REF_LINKS=0
_S_BODY_LINES=0
_S_BODY_TOKENS=0
_S_HAS_REFS_DIR=false
_S_REFS_LINKED=0
_S_REFS_TOTAL=0
_S_FENCES_OK=true
_S_SECRETS=0
_S_HARDCODED=0
_S_DANGEROUS=0
QUALITY_SCORE=0
QUALITY_GRADE="F"
QUALITY_DESC=0
QUALITY_CLARITY=0
QUALITY_SPEC=0
QUALITY_PROGRESSIVE=0
QUALITY_SECURITY=0
# ── Colors & formatting ───────────────────────────────────────────
if [ "$NO_COLOR" = "1" ]; then
_red() { printf '%s' "$*"; }
_green() { printf '%s' "$*"; }
_yellow() { printf '%s' "$*"; }
_blue() { printf '%s' "$*"; }
_cyan() { printf '%s' "$*"; }
_magenta() { printf '%s' "$*"; }
_bold() { printf '%s' "$*"; }
_dim() { printf '%s' "$*"; }
_bold_green() { printf '%s' "$*"; }
_bold_red() { printf '%s' "$*"; }
_bold_yellow() { printf '%s' "$*"; }
_bold_cyan() { printf '%s' "$*"; }
else
_red() { printf "\033[0;31m%s\033[0m" "$*"; }
_green() { printf "\033[0;32m%s\033[0m" "$*"; }
_yellow() { printf "\033[0;33m%s\033[0m" "$*"; }
_blue() { printf "\033[0;34m%s\033[0m" "$*"; }
_cyan() { printf "\033[0;36m%s\033[0m" "$*"; }
_magenta() { printf "\033[0;35m%s\033[0m" "$*"; }
_bold() { printf "\033[1m%s\033[0m" "$*"; }
_dim() { printf "\033[2m%s\033[0m" "$*"; }
_bold_green() { printf "\033[1;32m%s\033[0m" "$*"; }
_bold_red() { printf "\033[1;31m%s\033[0m" "$*"; }
_bold_yellow() { printf "\033[1;33m%s\033[0m" "$*"; }
_bold_cyan() { printf "\033[1;36m%s\033[0m" "$*"; }
fi
# ── Logging ────────────────────────────────────────────────────────
_error() {
ERRORS=$((ERRORS + 1))
[ "$OUTPUT_MODE" = "terminal" ] && echo " $(_red "✗ ERROR") $*" || true
echo "ERROR|${CURRENT_SECTION}|$*" >> "$FINDINGS_FILE"
}
_warn() {
WARNINGS=$((WARNINGS + 1))
[ "$OUTPUT_MODE" = "terminal" ] && echo " $(_yellow "! WARN ") $*" || true
echo "WARN|${CURRENT_SECTION}|$*" >> "$FINDINGS_FILE"
}
_pass() {
[ "$OUTPUT_MODE" = "terminal" ] && echo " $(_green "✓ PASS ") $*" || true
}
_info() {
INFO_COUNT=$((INFO_COUNT + 1))
[ "$OUTPUT_MODE" = "terminal" ] && echo " $(_blue " INFO ") $*" || true
echo "INFO|${CURRENT_SECTION}|$*" >> "$FINDINGS_FILE"
}
_detail() {
[ "$OUTPUT_MODE" = "terminal" ] && echo " $(_dim "$*")" || true
}
ci_annotate() {
local level="$1"; shift
if [ "$CI" = "true" ]; then
echo "::${level} $*"
fi
}
section() {
CHECKS_RUN=$((CHECKS_RUN + 1))
CURRENT_SECTION="$1"
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo ""
echo " $(_bold_cyan "[$CHECKS_RUN/$TOTAL_CHECKS]") $(_bold "$1")"
echo " $(_dim "$(printf '%.0s─' $(seq 1 60))")"
fi
}
file_lines() { wc -l < "$1" | tr -d ' '; }
file_chars() { wc -c < "$1" | tr -d ' '; }
file_words() { wc -w < "$1" | tr -d ' '; }
estimate_tokens() {
local chars
chars=$(file_chars "$1")
echo $(( chars / 4 ))
}
estimate_tokens_detailed() {
local file="$1"
local chars words lines char_tokens word_tokens
chars=$(file_chars "$file")
words=$(file_words "$file")
lines=$(file_lines "$file")
char_tokens=$(( chars / 4 ))
word_tokens=$(( (words * 13) / 10 ))
echo "$char_tokens|$chars|$words|$lines|$word_tokens"
}
token_zone_color() {
local tokens="$1" safe="$2" warn="$3"
if [ "$tokens" -le "$safe" ]; then
echo "green"
elif [ "$tokens" -le "$warn" ]; then
echo "yellow"
else
echo "red"
fi
}
format_tokens_colored() {
local tokens="$1" safe="$2" warn="$3" label="${4:-tokens}"
local zone
zone=$(token_zone_color "$tokens" "$safe" "$warn")
case "$zone" in
green) printf "%s" "$(_green "~$tokens $label")" ;;
yellow) printf "%s" "$(_yellow "~$tokens $label")" ;;
red) printf "%s" "$(_red "~$tokens $label")" ;;
esac
}
token_zone_indicator() {
local tokens="$1" safe="$2" warn="$3"
local zone pct
zone=$(token_zone_color "$tokens" "$safe" "$warn")
pct=$(( (tokens * 100) / warn ))
case "$zone" in
green)
if [ "$NO_COLOR" = "1" ]; then echo "[SAFE $pct%]"
else echo "$(_green "[SAFE $pct%]")"; fi ;;
yellow)
if [ "$NO_COLOR" = "1" ]; then echo "[WARN $pct%]"
else echo "$(_yellow "[WARN $pct%]")"; fi ;;
red)
if [ "$NO_COLOR" = "1" ]; then echo "[HIGH $pct%]"
else echo "$(_red "[HIGH $pct%]")"; fi ;;
esac
}
# ── Visual helpers ─────────────────────────────────────────────────
bar_gauge() {
local value="$1" max="$2" width="${3:-20}"
local pct filled empty bar=""
if [ "$max" -le 0 ]; then pct=0
else pct=$(( (value * 100) / max )); fi
[ "$pct" -gt 100 ] && pct=100
filled=$(( (pct * width) / 100 ))
empty=$(( width - filled ))
local i
for ((i=0; i<filled; i++)); do bar+="█"; done
for ((i=0; i<empty; i++)); do bar+="░"; done
echo "$bar"
}
bar_gauge_colored() {
local value="$1" max="$2" width="${3:-20}" safe="${4:-0}" warn="${5:-0}"
local pct gauge
if [ "$max" -le 0 ]; then pct=0
else pct=$(( (value * 100) / max )); fi
[ "$pct" -gt 100 ] && pct=100
gauge=$(bar_gauge "$value" "$max" "$width")
if [ "$safe" -gt 0 ] && [ "$warn" -gt 0 ]; then
local zone
zone=$(token_zone_color "$value" "$safe" "$warn")
case "$zone" in
green) printf "%s %s" "$(_green "$gauge")" "$(_green "${pct}%")" ;;
yellow) printf "%s %s" "$(_yellow "$gauge")" "$(_yellow "${pct}%")" ;;
red) printf "%s %s" "$(_red "$gauge")" "$(_red "${pct}%")" ;;
esac
else
printf "%s %d%%" "$gauge" "$pct"
fi
}
letter_grade() {
local score="$1"
if [ "$score" -ge 95 ]; then echo "A+"
elif [ "$score" -ge 90 ]; then echo "A"
elif [ "$score" -ge 85 ]; then echo "A-"
elif [ "$score" -ge 80 ]; then echo "B+"
elif [ "$score" -ge 75 ]; then echo "B"
elif [ "$score" -ge 70 ]; then echo "B-"
elif [ "$score" -ge 65 ]; then echo "C+"
elif [ "$score" -ge 60 ]; then echo "C"
elif [ "$score" -ge 55 ]; then echo "C-"
elif [ "$score" -ge 50 ]; then echo "D"
else echo "F"
fi
}
grade_color() {
local score="$1" grade
grade=$(letter_grade "$score")
if [ "$score" -ge 85 ]; then printf "%s" "$(_bold_green "$grade")"
elif [ "$score" -ge 65 ]; then printf "%s" "$(_bold_yellow "$grade")"
else printf "%s" "$(_bold_red "$grade")"
fi
}
safe_count() {
local result
result=$("$@" 2>/dev/null | tr -d ' ' || true)
if [ -z "$result" ] || ! [[ "$result" =~ ^[0-9]+$ ]]; then
echo "0"
else
echo "$result"
fi
}
# ════════════════════════════════════════════════════════════════════
# CHECKS
# ════════════════════════════════════════════════════════════════════
# ── [1] Skill Structure ───────────────────────────────────────────
check_structure() {
section "Skill Structure"
if [ ! -f "$SKILL_FILE" ]; then
_error "SKILL.md not found — required by agentskills.io spec"
ci_annotate "error" "file=SKILL.md::SKILL.md not found"
return
fi
_pass "SKILL.md ($(file_lines "$SKILL_FILE") lines, $(file_chars "$SKILL_FILE") bytes)"
if [ ! -f "README.md" ]; then
_warn "README.md missing — recommended for discoverability and GitHub rendering"
ci_annotate "warning" "file=README.md::README.md missing (recommended)"
else
_pass "README.md ($(file_lines README.md) lines)"
fi
local dirs=("references" "scripts" "assets" "agents")
for dir in "${dirs[@]}"; do
if [ -d "$dir" ]; then
local count
count=$(find "$dir" -type f | wc -l | tr -d ' ')
_pass "$dir/ ($count files)"
if [ "$dir" = "references" ]; then
_S_HAS_REFS_DIR=true
fi
fi
done
if [ ! -d "references" ]; then
_info "No references/ directory"
_detail "Add reference docs for progressive disclosure of complex content"
fi
if [ -f "LICENSE" ] || [ -f "LICENSE.md" ] || [ -f "LICENSE.txt" ]; then
_pass "License file found"
fi
}
# ── [2] Frontmatter ───────────────────────────────────────────────
check_frontmatter() {
[ ! -f "$SKILL_FILE" ] && return
section "Frontmatter & Description Quality"
local first_line
first_line=$(head -1 "$SKILL_FILE")
if [ "$first_line" != "---" ]; then
_error "Line 1: Expected '---' delimiter, found: '$first_line'"
ci_annotate "error" "file=$SKILL_FILE,line=1::Missing YAML frontmatter delimiter"
return
fi
local frontmatter
frontmatter=$(sed -n '2,/^---$/p' "$SKILL_FILE" | sed '$d')
# ── name ──
local name
name=$(echo "$frontmatter" | grep '^name:' | head -1 | sed 's/^name:[[:space:]]*//')
if [ -z "$name" ]; then
_error "Required field 'name' missing"
ci_annotate "error" "file=$SKILL_FILE::Missing required 'name' field"
else
_S_HAS_NAME=true
local name_len=${#name}
local name_ok=true
if [ "$name_len" -gt "$NAME_MAX_LEN" ]; then
_error "name '$name' is $name_len chars (max $NAME_MAX_LEN)"
ci_annotate "error" "file=$SKILL_FILE::name exceeds $NAME_MAX_LEN chars"
name_ok=false
fi
if echo "$name" | grep -qE '[A-Z]'; then
_error "name '$name' has uppercase — spec requires lowercase only"
ci_annotate "error" "file=$SKILL_FILE::name must be lowercase"
name_ok=false
fi
if echo "$name" | grep -qE '[^a-z0-9-]'; then
_error "name '$name' has invalid chars — only a-z, 0-9, hyphens allowed"
ci_annotate "error" "file=$SKILL_FILE::name contains invalid characters"
name_ok=false
fi
if echo "$name" | grep -qE '^-|-$'; then
_error "name '$name' starts or ends with hyphen"
ci_annotate "error" "file=$SKILL_FILE::name starts/ends with hyphen"
name_ok=false
fi
if echo "$name" | grep -qF -- '--'; then
_error "name '$name' has consecutive hyphens (--)"
ci_annotate "error" "file=$SKILL_FILE::name has consecutive hyphens"
name_ok=false
fi
if [ "$name_ok" = true ]; then
_pass "name: '$name' ($name_len chars)"
_S_NAME_OK=true
fi
if [ "$name" != "$SKILL_DIR" ]; then
_warn "name '$name' ≠ directory '$SKILL_DIR'"
_detail "Spec: name should match parent directory name"
ci_annotate "warning" "file=$SKILL_FILE::name doesn't match directory name"
else
_pass "name matches directory name"
fi
fi
# ── description ──
local desc_line desc=""
desc_line=$(echo "$frontmatter" | grep -n '^description:' | head -1 | cut -d: -f1)
if [ -z "$desc_line" ]; then
_error "Required field 'description' missing"
ci_annotate "error" "file=$SKILL_FILE::Missing required 'description' field"
else
_S_HAS_DESC=true
local inline_desc
inline_desc=$(echo "$frontmatter" | sed -n "${desc_line}p" | sed 's/^description:[[:space:]]*//')
if [ -n "$inline_desc" ] && ! echo "$inline_desc" | grep -qE '^\s*[>|]\s*$'; then
desc="$inline_desc"
else
desc=$(echo "$frontmatter" | sed -n "$((desc_line+1)),\$p" | sed '/^[a-zA-Z_-]*:/,$d' | tr '\n' ' ' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
fi
local desc_len=${#desc}
_S_DESC_LEN=$desc_len
if [ "$desc_len" -eq 0 ]; then
_error "description is empty"
ci_annotate "error" "file=$SKILL_FILE::description is empty"
else
if [ "$desc_len" -gt "$DESC_MAX_LEN" ]; then
_warn "description is $desc_len chars (spec max: $DESC_MAX_LEN)"
ci_annotate "warning" "file=$SKILL_FILE::description exceeds $DESC_MAX_LEN chars"
else
_pass "description length: $desc_len chars (limit: $DESC_MAX_LEN)"
fi
if [ "$desc_len" -lt "$DESC_MIN_USEFUL" ]; then
_warn "description is very short ($desc_len chars) — likely won't trigger well"
_detail "Good: 'Extract text and tables from PDF files, fill forms, merge"
_detail " documents. Use when working with PDF documents.'"
_detail "Bad: 'Helps with PDFs.'"
ci_annotate "warning" "file=$SKILL_FILE::description too short for reliable triggering"
fi
# WHAT and WHEN analysis
local has_what=false has_when=false
if echo "$desc" | grep -qiE 'build|create|generate|extract|analyze|process|manage|handle|configure|review|refactor|optimize|test|debug|deploy|format|validate|convert|transform|monitor|implement'; then
has_what=true
_S_DESC_WHAT=true
fi
if echo "$desc" | grep -qiE 'use when|when working|when the user|when handling|if the user|for tasks|for working|designed for|use this|use for'; then
has_when=true
_S_DESC_WHEN=true
fi
if [ "$has_what" = true ] && [ "$has_when" = true ]; then
_pass "description covers WHAT and WHEN to use"
elif [ "$has_what" = false ] && [ "$has_when" = false ]; then
_warn "description may lack WHAT the skill does and WHEN to use it"
_detail "Spec: 'Describes what the skill does and when to use it'"
elif [ "$has_when" = false ]; then
_info "description explains WHAT but could clarify WHEN to trigger"
_detail "Adding 'Use when...' helps agents decide when to activate"
fi
# ── [NEW] First-person voice ──
if echo "$desc" | grep -qiE '\bI can\b|\bI will\b|\bI help\b|\bI am\b|\bI provide\b|\bmy skill\b'; then
_warn "description uses first-person voice ('I can', 'I will', etc.)"
_detail "Descriptions are injected into the agent system prompt — first-person"
_detail "mixes viewpoints and confuses agent reasoning. Use third person instead"
ci_annotate "warning" "file=$SKILL_FILE::description uses first-person voice"
_S_DESC_FIRST_PERSON=true
else
_pass "description uses correct voice (not first-person)"
fi
# ── [NEW] Generic verb detection ──
local generic_matches
generic_matches=$(echo "$desc" | grep -oiE '\b(manage|handle|deal with|work with|help with|assist with|take care of)\b' 2>/dev/null | head -3 || true)
if [ -n "$generic_matches" ]; then
local generic_list
generic_list=$(echo "$generic_matches" | tr '\n' ', ' | sed 's/,$//')
_info "description contains generic verbs: $generic_list"
_detail "Specific verbs (extract, generate, validate) improve trigger precision"
_S_DESC_GENERIC=true
else
_pass "description uses specific action verbs"
fi
# ── [NEW] Negative trigger detection ──
if echo "$desc" | grep -qiE 'not for|not designed for|do not use|does not|don.t use'; then
_pass "description includes boundary markers (negative triggers)"
_S_DESC_NEGATIVES=true
else
_info "no negative triggers ('NOT for...', 'Do not use when...')"
_detail "Negative triggers prevent mis-activation when many skills are loaded"
fi
fi
fi
# ── optional fields ──
echo ""
_detail "Optional fields:"
if echo "$frontmatter" | grep -q '^license:'; then
local license_val
license_val=$(echo "$frontmatter" | grep '^license:' | sed 's/^license:[[:space:]]*//')
_pass "license: $license_val"
else
_info "No license field — recommended for shared/public skills"
fi
if echo "$frontmatter" | grep -q '^compatibility:'; then
local compat
compat=$(echo "$frontmatter" | grep '^compatibility:' | sed 's/^compatibility:[[:space:]]*//')
local compat_len=${#compat}
if [ "$compat_len" -gt "$COMPAT_MAX_LEN" ]; then
_warn "compatibility is $compat_len chars (spec max: $COMPAT_MAX_LEN)"
else
_pass "compatibility field present ($compat_len chars)"
fi
fi
if echo "$frontmatter" | grep -q '^metadata:'; then
_pass "metadata field present"
if echo "$frontmatter" | grep -q '^\s*version:'; then
_pass "metadata.version set"
fi
if echo "$frontmatter" | grep -q '^\s*author:'; then
_pass "metadata.author set"
fi
fi
if echo "$frontmatter" | grep -q '^allowed-tools:'; then
_pass "allowed-tools field present (experimental)"
fi
}
# ── [3] Body Content & Progressive Disclosure ─────────────────────
check_body() {
[ ! -f "$SKILL_FILE" ] && return
section "Body Content & Progressive Disclosure"
local body_start
body_start=$(grep -n '^---$' "$SKILL_FILE" | sed -n '2p' | cut -d: -f1)
if [ -z "$body_start" ]; then
_error "No closing frontmatter delimiter (---) found"
ci_annotate "error" "file=$SKILL_FILE::Missing closing frontmatter delimiter"
return
fi
local body_lines token_est
body_lines=$(tail -n +"$((body_start + 1))" "$SKILL_FILE" | wc -l | tr -d ' ')
token_est=$(estimate_tokens "$SKILL_FILE")
_S_BODY_LINES=$body_lines
_S_BODY_TOKENS=$token_est
# Line count with bar gauge
if [ "$body_lines" -eq 0 ]; then
_error "SKILL.md body is empty — agents need instructions"
ci_annotate "error" "file=$SKILL_FILE::Empty body"
elif [ "$body_lines" -gt "$BODY_MAX_LINES" ]; then
_warn "Body: $body_lines lines (spec recommends <$BODY_MAX_LINES)"
_detail "Move detailed content to references/ for on-demand loading"
ci_annotate "warning" "file=$SKILL_FILE::Body exceeds $BODY_MAX_LINES lines"
else
_pass "Body: $body_lines lines (limit: $BODY_MAX_LINES)"
fi
local lines_gauge
lines_gauge=$(bar_gauge_colored "$body_lines" "$BODY_MAX_LINES" 20 "$(( BODY_MAX_LINES * 70 / 100 ))" "$BODY_MAX_LINES")
[ "$OUTPUT_MODE" = "terminal" ] && echo " $lines_gauge" || true
# Token budget with bar gauge
if [ "$token_est" -gt "$TOKEN_BUDGET" ]; then
_warn "~$token_est tokens (spec recommends <$TOKEN_BUDGET on activation)"
_detail "Progressive disclosure: SKILL.md body loads fully on activation"
_detail "Keep instructions concise, move reference material to references/"
ci_annotate "warning" "file=$SKILL_FILE::Estimated $token_est tokens exceeds $TOKEN_BUDGET budget"
else
_pass "~$token_est tokens (budget: $TOKEN_BUDGET)"
fi
local token_gauge
token_gauge=$(bar_gauge_colored "$token_est" "$TOKEN_DANGER_ZONE" 20 "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE")
[ "$OUTPUT_MODE" = "terminal" ] && echo " $token_gauge" || true
# Structure analysis
local heading_count
heading_count=$(tail -n +"$((body_start + 1))" "$SKILL_FILE" | grep -c '^#' 2>/dev/null || true)
heading_count=${heading_count:-0}
_S_HEADINGS=$heading_count
if [ "$heading_count" -eq 0 ]; then
_warn "No headings in body — add structure for readability"
else
_pass "$heading_count heading(s) providing structure"
fi
# Code examples
local fence_pattern='```'
local code_fence_count
code_fence_count=$(grep -cF "$fence_pattern" "$SKILL_FILE" 2>/dev/null || true)
code_fence_count=${code_fence_count:-0}
local code_pairs=$(( code_fence_count / 2 ))
_S_CODE_BLOCKS=$code_pairs
local inline_code_count
inline_code_count=$(grep -c '`[^`]' "$SKILL_FILE" 2>/dev/null || true)
inline_code_count=${inline_code_count:-0}
if [ "$code_pairs" -gt 0 ]; then
_pass "$code_pairs fenced code example(s) in body"
elif [ "$inline_code_count" -gt 0 ]; then
_pass "$inline_code_count line(s) with inline code references"
else
_info "No code examples in body — consider adding for clarity"
fi
# Reference links (progressive disclosure pattern)
local ref_link_count
ref_link_count=$(tail -n +"$((body_start + 1))" "$SKILL_FILE" | grep -cE '\]\(references/' 2>/dev/null || true)
ref_link_count=${ref_link_count:-0}
_S_REF_LINKS=$ref_link_count
if [ "$ref_link_count" -gt 0 ]; then
_pass "$ref_link_count reference link(s) — using progressive disclosure"
elif [ -d "references" ]; then
local ref_file_count
ref_file_count=$(find references -name '*.md' -type f | wc -l | tr -d ' ')
if [ "$ref_file_count" -gt 0 ]; then
_warn "references/ has $ref_file_count files but body has no links to them"
_detail "Link references from SKILL.md body so agents can load them on demand"
fi
fi
}
# ── [4] Internal Links ────────────────────────────────────────────
check_links() {
[ ! -f "$SKILL_FILE" ] && return
section "Internal Links"
local link_data
link_data=$(grep -nF '](' "$SKILL_FILE" || true)
if [ -z "$link_data" ]; then
_info "No internal links in SKILL.md"
return
fi
local checked=0 broken=0
while IFS= read -r match; do
local line_num line_content
line_num=$(echo "$match" | cut -d: -f1)
line_content=$(echo "$match" | cut -d: -f2-)
local targets
targets=$(echo "$line_content" | tr ']' '\n' | grep '^(' | sed 's/^(\([^)]*\)).*/\1/' | sed '/^$/d')
[ -z "$targets" ] && continue
while IFS= read -r link_path; do
case "$link_path" in http*|https*|"#"*|"") continue ;; esac
local file_path
file_path=$(echo "$link_path" | cut -d'#' -f1)
[ -z "$file_path" ] && continue
checked=$((checked + 1))
if [ -f "$file_path" ]; then
_pass "Line $line_num: $link_path$(file_lines "$file_path") lines"
else
_error "Line $line_num: $link_path → FILE NOT FOUND"
ci_annotate "error" "file=$SKILL_FILE,line=$line_num::Broken link: $link_path"
broken=$((broken + 1))
fi
done <<< "$targets"
done <<< "$link_data"
echo ""
_detail "Checked $checked link(s), $broken broken"
}
# ── [5] Reference Files ──────────────────────────────────────────
check_references() {
[ ! -d "references" ] && return
section "Reference Files"
local ref_links=""
if [ -f "$SKILL_FILE" ]; then
ref_links=$(grep -oE '\]\(references/[^)]+\)' "$SKILL_FILE" | sed 's/\](\(.*\))/\1/' | cut -d'#' -f1 | sort -u || true)
fi
local total=0 linked=0 orphaned=0 empty=0
echo ""
printf " $(_dim " %-35s %-8s %-8s %s")\n" "FILE" "LINES" "STATUS" ""
echo " $(_dim " $(printf '%.0s─' $(seq 1 56))")"
while IFS= read -r file; do
total=$((total + 1))
local lines
lines=$(file_lines "$file")
if [ "$lines" -eq 0 ]; then
printf " $(_yellow " %-35s %-8s %-8s")\n" "$(basename "$file")" "0" "EMPTY"
_warn "$file is empty (0 lines)"
ci_annotate "warning" "file=$file::Empty reference file"
empty=$((empty + 1))
continue
fi
if echo "$ref_links" | grep -qF "$file"; then
linked=$((linked + 1))
printf " $(_green " %-35s %-8s %-8s")\n" "$(basename "$file")" "$lines" "LINKED"
else
orphaned=$((orphaned + 1))
printf " $(_yellow " %-35s %-8s %-8s")\n" "$(basename "$file")" "$lines" "ORPHAN"
_warn "$file$(_yellow "orphaned")"
_detail "Not linked from SKILL.md — agents won't discover this file"
ci_annotate "warning" "file=$file::Not linked from SKILL.md (orphaned)"
fi
done < <(find references -name '*.md' -type f | sort)
_S_REFS_TOTAL=$total
_S_REFS_LINKED=$linked
# Missing references (linked in SKILL.md but don't exist)
local missing=0
if [ -n "$ref_links" ]; then
for ref in $ref_links; do
if [ ! -f "$ref" ]; then
_error "$ref → linked in SKILL.md but file is missing"
ci_annotate "error" "file=$ref::Referenced in SKILL.md but does not exist"
missing=$((missing + 1))
fi
done
fi
echo ""
echo " $(_dim " ┌──────────────────────────────────────────┐")"
printf " $(_dim " │") Total: %-4s Linked: %-4s Orphaned: %-4s$(_dim "│")\n" "$total" "$linked" "$orphaned"
printf " $(_dim " │") Empty: %-4s Missing: %-4s $(_dim "│")\n" "$empty" "$missing"
echo " $(_dim " └──────────────────────────────────────────┘")"
}
# ── [6] Markdown Syntax ──────────────────────────────────────────
check_markdown() {
section "Markdown Syntax"
local pattern file_count=0 issues=0
pattern='```'
while IFS= read -r file; do
file_count=$((file_count + 1))
local count
count=$(grep -cF "$pattern" "$file" 2>/dev/null || true)
count=${count:-0}
if [ "$count" -gt 0 ] && [ $((count % 2)) -ne 0 ]; then
_warn "$file — unclosed code block ($count fences)"
grep -nF "$pattern" "$file" 2>/dev/null | while IFS= read -r m; do
_detail "Line $(echo "$m" | cut -d: -f1): $(echo "$m" | cut -d: -f2-)"
done
ci_annotate "warning" "file=$file::Unclosed code block ($count fences)"
issues=$((issues + 1))
fi
done < <(find . -name '*.md' -not -path './.git/*' | sort)
if [ "$issues" -eq 0 ]; then
_pass "All $file_count markdown files have balanced code fences"
else
_S_FENCES_OK=false
fi
}
# ── [7] Reference Nesting ────────────────────────────────────────
check_reference_depth() {
[ ! -d "references" ] && return
section "Reference Nesting"
local deep_refs=0
while IFS= read -r file; do
local nested
nested=$(grep -cE '\]\(references/' "$file" 2>/dev/null || true)
if [ "$nested" -gt 0 ]; then
_warn "$file$nested cross-reference(s) to other reference files"
_detail "Spec: keep file references one level deep from SKILL.md"
ci_annotate "warning" "file=$file::Cross-references between reference files"
deep_refs=$((deep_refs + 1))
fi
done < <(find references -name '*.md' -type f | sort)
if [ "$deep_refs" -eq 0 ]; then
_pass "No nested reference chains — clean one-level structure"
else
_detail "$deep_refs file(s) with cross-references"
fi
}
# ── [8] Scripts Validation ───────────────────────────────────────
check_scripts() {
[ ! -d "scripts" ] && return
section "Scripts"
local total=0 executable=0 not_executable=0 documented=0
while IFS= read -r script; do
total=$((total + 1))
local basename_script
basename_script=$(basename "$script")
if [ -x "$script" ]; then
executable=$((executable + 1))
_pass "$script (executable, $(file_lines "$script") lines)"
else
not_executable=$((not_executable + 1))
_warn "$script is not executable"
_detail "Run: chmod +x $script"
ci_annotate "warning" "file=$script::Script is not executable"
fi
if [ -f "$SKILL_FILE" ] && grep -qF "$basename_script" "$SKILL_FILE"; then
documented=$((documented + 1))
fi
done < <(find scripts -type f | sort)
if [ "$total" -gt 0 ] && [ "$documented" -eq 0 ] && [ -f "$SKILL_FILE" ]; then
_info "No scripts referenced in SKILL.md body"
_detail "Document available scripts so agents know what tools they can run"
elif [ "$documented" -gt 0 ]; then
_pass "$documented of $total script(s) documented in SKILL.md"
fi
}
# ── [9] Package Integrity ────────────────────────────────────────
check_repo_hygiene() {
section "Repository Hygiene"
local sensitive
sensitive=$(find . -maxdepth 3 \
\( -name '.env' -o -name '*.key' -o -name '*.pem' -o -name 'credentials*' \) \
-not -path './.git/*' 2>/dev/null || true)
if [ -n "$sensitive" ]; then
_warn "Possible sensitive files detected"
echo "$sensitive" | while IFS= read -r f; do
_detail " $f"
done
else
_pass "No sensitive files (.env, .key, .pem, credentials)"
fi
local artifacts
artifacts=$(find . -maxdepth 3 \
\( -name 'node_modules' -o -name '__pycache__' -o -name '.DS_Store' \) \
-not -path './.git/*' 2>/dev/null || true)
if [ -n "$artifacts" ]; then
_warn "Development artifacts found — add to .gitignore"
echo "$artifacts" | while IFS= read -r f; do
_detail " $f"
done
else
_pass "No development artifacts (node_modules, __pycache__, .DS_Store)"
fi
}
# ── [10] Token Budget Analysis ────────────────────────────────────
check_token_budget() {
section "Token Budget Analysis"
if [ "$OUTPUT_MODE" = "terminal" ]; then echo ""; fi
_detail "Token estimation: chars/4 (tiktoken cl100k_base approximation)"
_detail "Zone thresholds based on agentskills.io and context engineering best practices"
if [ "$OUTPUT_MODE" = "terminal" ]; then echo ""; fi
local total_tokens=0
local total_chars=0
local total_words=0
local total_lines=0
local file_count=0
# ── SKILL.md analysis ──
if [ -f "$SKILL_FILE" ]; then
local skill_data skill_tokens skill_chars skill_words skill_lines
skill_data=$(estimate_tokens_detailed "$SKILL_FILE")
skill_tokens=$(echo "$skill_data" | cut -d'|' -f1)
skill_chars=$(echo "$skill_data" | cut -d'|' -f2)
skill_words=$(echo "$skill_data" | cut -d'|' -f3)
skill_lines=$(echo "$skill_data" | cut -d'|' -f4)
local zone_indicator
zone_indicator=$(token_zone_indicator "$skill_tokens" "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE")
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo " $(_bold "SKILL.md") $zone_indicator"
echo " ├─ $(format_tokens_colored "$skill_tokens" "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE" "tokens")"
local skill_gauge
skill_gauge=$(bar_gauge_colored "$skill_tokens" "$TOKEN_DANGER_ZONE" 20 "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE")
echo " ├─ $skill_gauge"
echo " ├─ $skill_chars chars | $skill_words words | $skill_lines lines"
echo " └─ Budget: $TOKEN_WARN_ZONE tokens recommended"
fi
total_tokens=$((total_tokens + skill_tokens))
total_chars=$((total_chars + skill_chars))
total_words=$((total_words + skill_words))
total_lines=$((total_lines + skill_lines))
file_count=$((file_count + 1))
if [ "$skill_tokens" -gt "$TOKEN_DANGER_ZONE" ]; then
_warn "SKILL.md exceeds danger zone (~$skill_tokens tokens > $TOKEN_DANGER_ZONE)"
_detail "Consider moving content to references/ for on-demand loading"
fi
fi
# ── Reference files analysis ──
if [ -d "references" ]; then
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo ""
echo " $(_bold "Reference Files:")"
echo ""
printf " $(_dim "%-35s %-12s %-6s %-6s")\n" "FILE" "ZONE" "TOKENS" "LINES"
echo " $(_dim "$(printf '%.0s─' $(seq 1 62))")"
fi
local ref_total=0
local ref_files=0
local largest_ref=""
local largest_ref_tokens=0
while IFS= read -r file; do
local ref_data ref_tokens ref_chars ref_words ref_lines
ref_data=$(estimate_tokens_detailed "$file")
ref_tokens=$(echo "$ref_data" | cut -d'|' -f1)
ref_chars=$(echo "$ref_data" | cut -d'|' -f2)
ref_words=$(echo "$ref_data" | cut -d'|' -f3)
ref_lines=$(echo "$ref_data" | cut -d'|' -f4)
local zone_indicator
zone_indicator=$(token_zone_indicator "$ref_tokens" "$REF_TOKEN_SAFE" "$REF_TOKEN_WARN")
local basename_file
basename_file=$(basename "$file")
[ "$OUTPUT_MODE" = "terminal" ] && printf " %-35s %-12s ~%-6d %d\n" "$basename_file" "$(echo "$zone_indicator" | sed 's/\x1b\[[0-9;]*m//g')" "$ref_tokens" "$ref_lines" || true
ref_total=$((ref_total + ref_tokens))
ref_files=$((ref_files + 1))
total_tokens=$((total_tokens + ref_tokens))
total_chars=$((total_chars + ref_chars))
total_words=$((total_words + ref_words))
total_lines=$((total_lines + ref_lines))
file_count=$((file_count + 1))
if [ "$ref_tokens" -gt "$largest_ref_tokens" ]; then
largest_ref_tokens=$ref_tokens
largest_ref="$basename_file"
fi
if [ "$ref_tokens" -gt "$REF_TOKEN_DANGER" ]; then
_warn "$basename_file is too large (~$ref_tokens tokens > $REF_TOKEN_DANGER)"
_detail "Split into base + advanced files (e.g., topic.md + topic-advanced.md)"
elif [ "$ref_tokens" -gt "$REF_TOKEN_WARN" ]; then
_warn "$basename_file is getting large (~$ref_tokens tokens > $REF_TOKEN_WARN)"
_detail "Tighten prose: tables over paragraphs, cross-link shared patterns, trim tutorial content"
fi
done < <(find references -name '*.md' -type f | sort)
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo ""
echo " $(_dim "Reference subtotal: ~$ref_total tokens across $ref_files files")"
if [ -n "$largest_ref" ]; then
echo " $(_dim "Largest reference: $largest_ref (~$largest_ref_tokens tokens)")"
fi
fi
fi
# ── README analysis ──
if [ -f "README.md" ]; then
local readme_data readme_tokens readme_lines
readme_data=$(estimate_tokens_detailed "README.md")
readme_tokens=$(echo "$readme_data" | cut -d'|' -f1)
readme_lines=$(echo "$readme_data" | cut -d'|' -f4)
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo ""
echo " $(_bold "README.md") $(_dim "(not loaded by agents, for humans)")"
echo " └─ ~$readme_tokens tokens | $readme_lines lines"
fi
fi
# ── Total package summary ──
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo ""
echo " $(_bold "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")"
local total_zone_indicator
total_zone_indicator=$(token_zone_indicator "$total_tokens" "$TOTAL_TOKEN_SAFE" "$TOTAL_TOKEN_WARN")
echo " $(_bold "TOTAL SKILL PACKAGE") $total_zone_indicator"
echo ""
echo " $(format_tokens_colored "$total_tokens" "$TOTAL_TOKEN_SAFE" "$TOTAL_TOKEN_WARN" "tokens") (estimated)"
local total_gauge
total_gauge=$(bar_gauge_colored "$total_tokens" "$TOTAL_TOKEN_DANGER" 25 "$TOTAL_TOKEN_SAFE" "$TOTAL_TOKEN_WARN")
echo " $total_gauge"
echo " $total_chars chars | $total_words words | $total_lines lines | $file_count files"
echo ""
fi
local uses_progressive=false
if [ -d "references" ] && [ -f "$SKILL_FILE" ]; then
local ref_link_count
ref_link_count=$(grep -cE '\]\(references/' "$SKILL_FILE" 2>/dev/null || true)
ref_link_count=${ref_link_count:-0}
if [ "$ref_link_count" -gt 0 ]; then
uses_progressive=true
fi
fi
local ctx_200k_pct ctx_128k_pct skill_only_200k_pct
ctx_200k_pct=$(( (total_tokens * 100) / 200000 ))
ctx_128k_pct=$(( (total_tokens * 100) / 128000 ))
if [ -f "$SKILL_FILE" ]; then
skill_only_200k_pct=$(( (_S_BODY_TOKENS * 100) / 200000 ))
else
skill_only_200k_pct=0
fi
if [ "$OUTPUT_MODE" = "terminal" ]; then
echo " $(_bold "Context Window Impact:")"
echo " $(_dim "Agent tools (Cursor, Windsurf, Copilot) typically use ~200K context")"
echo " $(_dim "regardless of the underlying model's max window.")"
echo ""
echo " ├─ Agent context (200K): ~${ctx_200k_pct}% if all files loaded"
echo " ├─ Agent context (128K): ~${ctx_128k_pct}% if all files loaded"
if [ "$uses_progressive" = true ]; then
echo " ├─ $(_green "SKILL.md only: ~${skill_only_200k_pct}% of 200K") (initial activation cost)"
echo " └─ $(_dim "Progressive disclosure — references load on demand, not upfront")"
else
echo " └─ $(_yellow "Monolithic — entire skill loads at once (no references/ linked)")"
fi
echo ""
fi
if [ "$uses_progressive" = true ]; then
if [ "$total_tokens" -le "$TOTAL_TOKEN_SAFE" ]; then
_pass "Package is well-optimized for token efficiency"
elif [ "$total_tokens" -le "$TOTAL_TOKEN_WARN" ]; then
_pass "Package size is fine — progressive disclosure loads files on demand"
else
_info "Large package (~$total_tokens tokens) — ensure no dead/orphaned references"
_detail "Total size is informational with progressive disclosure; individual file sizes are the real quality gate"
fi
else
if [ "$total_tokens" -le "$TOKEN_BUDGET" ]; then
_pass "Monolithic skill is within token budget"
elif [ "$total_tokens" -le "$TOKEN_DANGER_ZONE" ]; then
_warn "Monolithic skill (~$total_tokens tokens) exceeds $TOKEN_BUDGET budget"
_detail "Move detailed content to references/ for on-demand loading"
else
_warn "Monolithic skill (~$total_tokens tokens) is very large"
_detail "Split into SKILL.md + references/ for progressive disclosure"
fi
fi
}
# ── [11] Content Quality Metrics ─────────────────────────────────
check_content_quality() {
section "Content Quality Metrics"
local total_code_blocks=0
local total_headings=0
if [ -f "$SKILL_FILE" ]; then
local code_blocks headings internal_links external_links
code_blocks=$(safe_count grep -c '```' "$SKILL_FILE")
code_blocks=$((code_blocks / 2))
headings=$(safe_count grep -c '^#' "$SKILL_FILE")
internal_links=$(safe_count grep -c '\](references/' "$SKILL_FILE")
external_links=$(safe_count grep -cE '\]\(https?://' "$SKILL_FILE")
echo ""
echo " $(_bold "SKILL.md Structure:")"
echo " ├─ $headings heading(s) providing navigation"
echo " ├─ $code_blocks code example(s)"
echo " ├─ $internal_links internal link(s) to references"
echo " └─ $external_links external link(s)"
total_code_blocks=$code_blocks
total_headings=$headings
if [ "$headings" -lt 3 ]; then
_info "Consider adding more headings for better navigation"
fi
if [ "$code_blocks" -lt 2 ]; then
_info "Consider adding code examples for clarity"
fi
fi
if [ -d "references" ]; then
echo ""
echo " $(_bold "Reference Files Quality:")"
local ref_with_code=0
local ref_without_code=0
local total_ref_headings=0
while IFS= read -r file; do
local file_code_blocks file_headings
file_code_blocks=$(safe_count grep -c '```' "$file")
file_code_blocks=$((file_code_blocks / 2))
file_headings=$(safe_count grep -c '^#' "$file")
total_code_blocks=$((total_code_blocks + file_code_blocks))
total_ref_headings=$((total_ref_headings + file_headings))
if [ "$file_code_blocks" -gt 0 ]; then
ref_with_code=$((ref_with_code + 1))
else
ref_without_code=$((ref_without_code + 1))
fi
done < <(find references -name '*.md' -type f)
local ref_count
ref_count=$(find references -name '*.md' -type f | wc -l | tr -d ' ')
echo " ├─ $ref_with_code of $ref_count files contain code examples"
echo " ├─ $total_ref_headings total headings across references"
echo " └─ $total_code_blocks total code blocks in package"
if [ "$ref_without_code" -gt 0 ]; then
_info "$ref_without_code reference file(s) have no code examples"
_detail "Code examples help agents understand expected patterns"
fi
fi
# Keyword density analysis
if [ -f "$SKILL_FILE" ]; then
echo ""
echo " $(_bold "Trigger Keyword Analysis:")"
local compose_mentions kotlin_mentions android_mentions kmp_mentions
compose_mentions=$(grep -ioE '(compose|composable|@Composable|jetpack)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
kotlin_mentions=$(grep -ioE '(kotlin|coroutine|flow|stateflow|channel)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
android_mentions=$(grep -ioE '(android|viewmodel|hilt|koin|room|datastore)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
kmp_mentions=$(grep -ioE '(multiplatform|kmp|cmp|commonmain|ios|desktop)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
compose_mentions=${compose_mentions:-0}
kotlin_mentions=${kotlin_mentions:-0}
android_mentions=${android_mentions:-0}
kmp_mentions=${kmp_mentions:-0}
echo " ├─ Compose/UI: $compose_mentions mentions"
echo " ├─ Kotlin/Flow: $kotlin_mentions mentions"
echo " ├─ Android/DI: $android_mentions mentions"
echo " └─ Multiplatform: $kmp_mentions mentions"
local total_keywords=$((compose_mentions + kotlin_mentions + android_mentions + kmp_mentions))
if [ "$total_keywords" -lt 10 ]; then
_info "Low keyword density may reduce skill trigger accuracy"
_detail "Ensure description and body mention key terms agents should recognize"
else
_pass "Good keyword coverage for skill triggering ($total_keywords mentions)"
fi
fi
}
# ── [12] Agent Metadata ──────────────────────────────────────────
check_agents_metadata() {
section "Agent Metadata (Codex)"
if [ ! -f "agents/openai.yaml" ]; then
_info "No agents/openai.yaml — optional, configures Codex app UI and policy"
return
fi
_pass "agents/openai.yaml present"
if grep -q '^interface:' agents/openai.yaml; then
if grep -q 'display_name:' agents/openai.yaml; then
local display_name
display_name=$(grep 'display_name:' agents/openai.yaml | sed 's/.*display_name:[[:space:]]*//' | tr -d '"')
_pass "display_name: '$display_name'"
else
_info "No display_name — Codex uses skill name"
fi
if grep -q 'short_description:' agents/openai.yaml; then
_pass "short_description set"
else
_info "No short_description — Codex uses SKILL.md description"
fi
if grep -q 'default_prompt:' agents/openai.yaml; then
_pass "default_prompt set"
fi
if grep -q 'brand_color:' agents/openai.yaml; then
_pass "brand_color set"
fi
local icon_fields
icon_fields=$(grep -oE '(icon_small|icon_large):[[:space:]]*"[^"]+"' agents/openai.yaml 2>/dev/null || true)
if [ -n "$icon_fields" ]; then
echo "$icon_fields" | while IFS= read -r line; do
local field icon_path
field=$(echo "$line" | cut -d: -f1 | tr -d ' ')
icon_path=$(echo "$line" | sed 's/.*"\(.*\)"/\1/')
if [ -f "$icon_path" ]; then
_pass "$field: $icon_path"
else
_warn "$field: '$icon_path' → file not found"
ci_annotate "warning" "file=agents/openai.yaml::$field file missing: $icon_path"
fi
done
fi
fi
if grep -q '^policy:' agents/openai.yaml; then
if grep -q 'allow_implicit_invocation:' agents/openai.yaml; then
local implicit
implicit=$(grep 'allow_implicit_invocation:' agents/openai.yaml | sed 's/.*allow_implicit_invocation:[[:space:]]*//')
_pass "allow_implicit_invocation: $implicit"
fi
fi
if grep -q '^dependencies:' agents/openai.yaml; then
_pass "dependencies declared"
fi
}
# ── [13] Security Scan ───────────────────────────────────────────
check_security() {
section "Security Scan"
local secret_count=0
local path_count=0
local cmd_count=0
# ── Secrets detection (11 patterns from skill-tools / skill-validator) ──
local -a secret_patterns=(
'sk-[a-zA-Z0-9]{20,}'
'sk_live_[a-zA-Z0-9]+'
'sk_test_[a-zA-Z0-9]+'
'ghp_[a-zA-Z0-9]{36}'
'gho_[a-zA-Z0-9]{36}'
'ghu_[a-zA-Z0-9]{36}'
'ghs_[a-zA-Z0-9]{36}'
'ghr_[a-zA-Z0-9]{36}'
'xoxb-[a-zA-Z0-9-]+'
'xoxp-[a-zA-Z0-9-]+'
'AKIA[0-9A-Z]{16}'
)
while IFS= read -r file; do
for pattern in "${secret_patterns[@]}"; do
local matches
matches=$(grep -nE "$pattern" "$file" 2>/dev/null | head -1 || true)
if [ -n "$matches" ]; then
secret_count=$((secret_count + 1))
local line_num
line_num=$(echo "$matches" | cut -d: -f1)
_error "$file:$line_num — potential secret/API key detected"
_detail "Pattern: $pattern"
ci_annotate "error" "file=$file,line=$line_num::Potential secret detected"
fi
done
# PEM private key headers
local pem_match
pem_match=$(grep -n 'BEGIN.*PRIVATE KEY' "$file" 2>/dev/null | head -1 || true)
if [ -n "$pem_match" ]; then
secret_count=$((secret_count + 1))
local line_num
line_num=$(echo "$pem_match" | cut -d: -f1)
_error "$file:$line_num — private key header detected"
ci_annotate "error" "file=$file,line=$line_num::Private key detected"
fi
# JWT tokens
local jwt_match
jwt_match=$(grep -nE 'eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.' "$file" 2>/dev/null | head -1 || true)
if [ -n "$jwt_match" ]; then
secret_count=$((secret_count + 1))
local line_num
line_num=$(echo "$jwt_match" | cut -d: -f1)
_error "$file:$line_num — JWT token detected"
ci_annotate "error" "file=$file,line=$line_num::JWT token detected"
fi
done < <(find . -name '*.md' -not -path './.git/*' | sort)
_S_SECRETS=$secret_count
if [ "$secret_count" -eq 0 ]; then
_pass "No API keys or secrets detected (13 patterns checked)"
fi
# ── Hardcoded absolute paths ──
echo ""
_detail "Checking for hardcoded paths..."
while IFS= read -r file; do
local path_matches
path_matches=$(grep -nE '(/Users/[a-zA-Z]|/home/[a-zA-Z]|C:\\Users\\)' "$file" 2>/dev/null || true)
if [ -n "$path_matches" ]; then
while IFS= read -r match; do
path_count=$((path_count + 1))
local line_num
line_num=$(echo "$match" | cut -d: -f1)
_warn "$file:$line_num — hardcoded absolute path"
_detail "$(echo "$match" | cut -d: -f2- | sed 's/^[[:space:]]*//' | head -c 80)"
done <<< "$path_matches"
fi
done < <(find . -name '*.md' -not -path './.git/*' | sort)
_S_HARDCODED=$path_count
if [ "$path_count" -eq 0 ]; then
_pass "No hardcoded absolute paths"
fi
# ── Dangerous commands ──
echo ""
_detail "Checking for dangerous commands..."
local -a dangerous_patterns=(
'rm -rf /'
'sudo rm '
'DROP TABLE'
'DROP DATABASE'
'chmod 777'
)
local -a dangerous_regex=(
'curl .*\| *sh'
'wget .*\| *sh'
)
while IFS= read -r file; do
for pattern in "${dangerous_patterns[@]}"; do
local matches
matches=$(grep -nF "$pattern" "$file" 2>/dev/null | head -1 || true)
if [ -n "$matches" ]; then
cmd_count=$((cmd_count + 1))
local line_num
line_num=$(echo "$matches" | cut -d: -f1)
_warn "$file:$line_num — potentially dangerous command"
_detail "Matched: $pattern"
ci_annotate "warning" "file=$file,line=$line_num::Dangerous command pattern"
fi
done
for pattern in "${dangerous_regex[@]}"; do
local matches
matches=$(grep -nE "$pattern" "$file" 2>/dev/null | head -1 || true)
if [ -n "$matches" ]; then
cmd_count=$((cmd_count + 1))
local line_num
line_num=$(echo "$matches" | cut -d: -f1)
_warn "$file:$line_num — potentially dangerous command"
_detail "Matched: $pattern"
ci_annotate "warning" "file=$file,line=$line_num::Dangerous command pattern"
fi
done
done < <(find . -name '*.md' -not -path './.git/*' | sort)
_S_DANGEROUS=$cmd_count
if [ "$cmd_count" -eq 0 ]; then
_pass "No dangerous shell commands detected"
fi
echo ""
echo " $(_dim " ┌─────────────────────────────────────────────┐")"
printf " $(_dim " │") Secrets: %-4s Paths: %-4s Commands: %-4s $(_dim "│")\n" "$secret_count" "$path_count" "$cmd_count"
echo " $(_dim " └─────────────────────────────────────────────┘")"
}
# ── [14] Heading Hierarchy & Duplicates ──────────────────────────
check_heading_hierarchy() {
section "Heading Hierarchy & Duplicates"
local skip_issues=0
local dup_issues=0
local files_checked=0
while IFS= read -r file; do
files_checked=$((files_checked + 1))
local prev_level=0
local line_num=0
local headings_seen=""
while IFS= read -r line; do
line_num=$((line_num + 1))
case "$line" in
'#'*)
local stripped_prefix
stripped_prefix="${line%%[^#]*}"
local level=${#stripped_prefix}
local text
text=$(echo "$line" | sed 's/^#* *//')
# MD001: heading level increment check
if [ "$prev_level" -gt 0 ] && [ "$level" -gt "$((prev_level + 1))" ]; then
skip_issues=$((skip_issues + 1))
_warn "$file:$line_num — heading skip H$prev_level → H$level"
_detail "$text"
ci_annotate "warning" "file=$file,line=$line_num::Heading level skip (H$prev_level to H$level)"
fi
prev_level=$level
# MD024: duplicate heading check (same level, same text)
local key="${level}:${text}"
if echo "$headings_seen" | grep -qF "<<${key}>>"; then
dup_issues=$((dup_issues + 1))
_warn "$file:$line_num — duplicate heading: '$text' (H$level)"
ci_annotate "warning" "file=$file,line=$line_num::Duplicate heading '$text'"
fi
headings_seen="${headings_seen}<<${key}>>"
;;
esac
done < "$file"
done < <(find . -name '*.md' -not -path './.git/*' | sort)
if [ "$skip_issues" -eq 0 ]; then
_pass "No heading level skips across $files_checked files (MD001)"
fi
if [ "$dup_issues" -eq 0 ]; then
_pass "No duplicate sibling headings across $files_checked files (MD024)"
fi
echo ""
_detail "Checked $files_checked files | Level skips: $skip_issues | Duplicates: $dup_issues"
}
# ── [15] Quality Score ───────────────────────────────────────────
compute_quality_score() {
section "Quality Score"
local desc_score=0
local clarity_score=0
local spec_score=0
local progressive_score=0
local security_score=10
_check_mark() {
if [ "$1" = "1" ]; then printf "%s" "$(_green "✓")"; else printf "%s" "$(_red "✗")"; fi
}
# ── Description Quality (30 pts) ──
local d1=0 d2=0 d3=0 d4=0 d5=0 d6=0 d7=0
[ "$_S_HAS_DESC" = true ] && { desc_score=$((desc_score + 5)); d1=1; }
[ "$_S_DESC_LEN" -ge "$DESC_MIN_USEFUL" ] && { desc_score=$((desc_score + 3)); d2=1; }
[ "$_S_DESC_WHAT" = true ] && { desc_score=$((desc_score + 5)); d3=1; }
[ "$_S_DESC_WHEN" = true ] && { desc_score=$((desc_score + 5)); d4=1; }
[ "$_S_DESC_FIRST_PERSON" = false ] && { desc_score=$((desc_score + 4)); d5=1; }
[ "$_S_DESC_GENERIC" = false ] && { desc_score=$((desc_score + 4)); d6=1; }
[ "$_S_DESC_NEGATIVES" = true ] && { desc_score=$((desc_score + 4)); d7=1; }
# ── Instruction Clarity (25 pts) ──
local c1=0 c2=0 c3=0 c4=0 c5=0 c6=0
[ "$_S_CODE_BLOCKS" -ge 1 ] && { clarity_score=$((clarity_score + 5)); c1=1; }
[ "$_S_CODE_BLOCKS" -ge 3 ] && { clarity_score=$((clarity_score + 5)); c2=1; }
[ "$_S_HEADINGS" -ge 3 ] && { clarity_score=$((clarity_score + 5)); c3=1; }
[ "$_S_HEADINGS" -ge 5 ] && { clarity_score=$((clarity_score + 3)); c4=1; }
[ "$_S_REF_LINKS" -gt 0 ] && { clarity_score=$((clarity_score + 4)); c5=1; }
[ "$_S_REF_LINKS" -ge 5 ] && { clarity_score=$((clarity_score + 3)); c6=1; }
# ── Spec Compliance (20 pts) ──
local s1=0 s2=0 s3=0 s4=0 s5=0 s6=0
[ "$_S_HAS_NAME" = true ] && { spec_score=$((spec_score + 4)); s1=1; }
[ "$_S_NAME_OK" = true ] && { spec_score=$((spec_score + 4)); s2=1; }
[ "$_S_HAS_DESC" = true ] && { spec_score=$((spec_score + 4)); s3=1; }
[ "$_S_BODY_TOKENS" -le "$TOKEN_BUDGET" ] && { spec_score=$((spec_score + 4)); s4=1; }
[ "$_S_BODY_LINES" -le "$BODY_MAX_LINES" ] && { spec_score=$((spec_score + 2)); s5=1; }
[ "$_S_FENCES_OK" = true ] && { spec_score=$((spec_score + 2)); s6=1; }
# ── Progressive Disclosure (15 pts) ──
local p1=0 p2=0 p3=0 p4=0
[ "$_S_HAS_REFS_DIR" = true ] && { progressive_score=$((progressive_score + 5)); p1=1; }
[ "$_S_REFS_LINKED" -gt 0 ] && { progressive_score=$((progressive_score + 5)); p2=1; }
if [ "$_S_REFS_TOTAL" -gt 0 ] && [ "$_S_REFS_LINKED" -eq "$_S_REFS_TOTAL" ]; then
progressive_score=$((progressive_score + 3)); p3=1
fi
[ "$_S_BODY_LINES" -lt "$BODY_MAX_LINES" ] && { progressive_score=$((progressive_score + 2)); p4=1; }
# ── Security (10 pts — start at 10, deduct for issues) ──
local x1=1 x2=1 x3=1
if [ "$_S_SECRETS" -gt 0 ]; then security_score=$((security_score - 5)); x1=0; fi
if [ "$_S_HARDCODED" -gt 0 ]; then security_score=$((security_score - 3)); x2=0; fi
if [ "$_S_DANGEROUS" -gt 0 ]; then security_score=$((security_score - 2)); x3=0; fi
[ "$security_score" -lt 0 ] && security_score=0
local total_score=$((desc_score + clarity_score + spec_score + progressive_score + security_score))
QUALITY_SCORE=$total_score
QUALITY_GRADE=$(letter_grade "$total_score")
QUALITY_DESC=$desc_score
QUALITY_CLARITY=$clarity_score
QUALITY_SPEC=$spec_score
QUALITY_PROGRESSIVE=$progressive_score
QUALITY_SECURITY=$security_score
# ── Display: clean bar chart overview (terminal only) ──
if [ "$OUTPUT_MODE" = "terminal" ]; then
_dim_bar() {
local name="$1" score="$2" max="$3"
local gauge pct
if [ "$max" -le 0 ]; then pct=0
else pct=$(( (score * 100) / max )); fi
gauge=$(bar_gauge "$score" "$max" 15)
local colored_gauge
if [ "$pct" -ge 80 ]; then
colored_gauge=$(_green "$gauge")
elif [ "$pct" -ge 60 ]; then
colored_gauge=$(_yellow "$gauge")
else
colored_gauge=$(_red "$gauge")
fi
if [ "$score" -eq "$max" ]; then
printf " %-24s %s %2d / %-2d $(_green "✓")\n" "$name" "$colored_gauge" "$score" "$max"
else
printf " %-24s %s %2d / %-2d\n" "$name" "$colored_gauge" "$score" "$max"
fi
}
echo ""
echo " $(_bold "Score Overview:")"
echo ""
_dim_bar "Description Quality" "$desc_score" "30"
_dim_bar "Instruction Clarity" "$clarity_score" "25"
_dim_bar "Spec Compliance" "$spec_score" "20"
_dim_bar "Progressive Disclosure" "$progressive_score" "15"
_dim_bar "Security" "$security_score" "10"
echo ""
echo " $(_dim "$(printf '%.0s─' $(seq 1 52))")"
local total_gauge
total_gauge=$(bar_gauge "$total_score" 100 20)
local colored_total
if [ "$total_score" -ge 85 ]; then
colored_total=$(_bold_green "$total_gauge")
elif [ "$total_score" -ge 65 ]; then
colored_total=$(_bold_yellow "$total_gauge")
else
colored_total=$(_bold_red "$total_gauge")
fi
printf " %-24s %s %d/100 Grade: %s\n" "$(_bold "TOTAL")" "$colored_total" "$total_score" "$(grade_color "$total_score")"
fi # end terminal-only display
# ── Collect failed sub-checks with sources and actionable fixes ──
# Sources verified against:
# [spec] agentskills.io/specification
# [official] agentskills.io/skill-creation/best-practices
# agentskills.io/skill-creation/optimizing-descriptions
# [community] mdskills.ai/docs/skill-best-practices
local fix_count=0
local -a fix_labels=()
local -a fix_sources=()
local -a fix_dims=()
[ "$d1" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Add a 'description:' field to frontmatter (+5)"); fix_sources+=("spec: required field — agentskills.io/specification#description-field"); }
[ "$d2" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Write a longer description (>=${DESC_MIN_USEFUL} chars) (+3)"); fix_sources+=("official: 'A few sentences to a short paragraph' — agentskills.io/skill-creation/optimizing-descriptions"); }
[ "$d3" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Add action verbs: build, generate, validate, extract, etc. (+5)"); fix_sources+=("spec: 'Describes what the skill does' — agentskills.io/specification#description-field"); }
[ "$d4" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Add 'Use when...' trigger phrase to description (+5)"); fix_sources+=("official: 'Use imperative phrasing: Use this skill when...' — agentskills.io/skill-creation/optimizing-descriptions"); }
[ "$d5" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Rewrite in third person — remove 'I can/will/help' (+4)"); fix_sources+=("community: 'first person causes discovery problems' — mdskills.ai/docs/skill-best-practices"); }
[ "$d6" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Replace generic verbs (manage, handle) with specific ones (+4)"); fix_sources+=("community: 'Be specific about format, operation, trigger' — mdskills.ai/docs/skill-best-practices"); }
[ "$d7" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Description"); fix_labels+=("Add boundary markers: 'Not for...', 'Do not use when...' (+4)"); fix_sources+=("official: 'Add specificity about what the skill does not do' — agentskills.io/skill-creation/optimizing-descriptions"); }
[ "$c1" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Clarity"); fix_labels+=("Add at least 1 fenced code block to SKILL.md body (+5)"); fix_sources+=("spec: recommended section 'Examples of inputs and outputs' — agentskills.io/specification#body-content"); }
[ "$c2" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Clarity"); fix_labels+=("Add 3+ code examples to SKILL.md (+5)"); fix_sources+=("official: 'a working example tends to outperform exhaustive documentation' — agentskills.io/skill-creation/best-practices"); }
[ "$c3" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Clarity"); fix_labels+=("Add 3+ markdown headings for navigation (+5)"); fix_sources+=("community: 'structure for agent scanning' — mdskills.ai/docs/skill-best-practices"); }
[ "$c4" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Clarity"); fix_labels+=("Add 5+ headings for deeper structure (+3)"); fix_sources+=("community: structured content aids agent navigation — mdskills.ai/docs/skill-best-practices"); }
[ "$c5" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Clarity"); fix_labels+=("Link to at least one reference file from SKILL.md body (+4)"); fix_sources+=("official: 'tell the agent when to load each file' — agentskills.io/skill-creation/best-practices"); }
[ "$c6" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Clarity"); fix_labels+=("Link to 5+ reference files for full coverage (+3)"); fix_sources+=("official: progressive disclosure via on-demand loading — agentskills.io/specification#progressive-disclosure"); }
[ "$s4" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Spec"); fix_labels+=("Reduce SKILL.md to <$TOKEN_BUDGET tokens — move content to references/ (+4)"); fix_sources+=("spec: '<5000 tokens recommended' — agentskills.io/specification#progressive-disclosure"); }
[ "$s5" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Spec"); fix_labels+=("Reduce SKILL.md body to <$BODY_MAX_LINES lines (+2)"); fix_sources+=("spec: 'under 500 lines' — agentskills.io/specification#progressive-disclosure"); }
[ "$s6" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Spec"); fix_labels+=("Fix unclosed code fences in markdown files (+2)"); fix_sources+=("spec: valid markdown required for agent parsing"); }
[ "$p1" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Disclosure"); fix_labels+=("Create a references/ directory for detailed docs (+5)"); fix_sources+=("spec: optional directory for on-demand loading — agentskills.io/specification#references"); }
[ "$p2" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Disclosure"); fix_labels+=("Add links to reference files from SKILL.md body (+5)"); fix_sources+=("official: 'tell the agent when to load each file' — agentskills.io/skill-creation/best-practices"); }
[ "$p3" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Disclosure"); fix_labels+=("Link all reference files from SKILL.md — orphaned files found (+3)"); fix_sources+=("spec: 'use relative paths from skill root' — agentskills.io/specification#file-references"); }
[ "$p4" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Disclosure"); fix_labels+=("Keep body under $BODY_MAX_LINES lines (+2)"); fix_sources+=("spec: 'under 500 lines' — agentskills.io/specification#progressive-disclosure"); }
[ "$x1" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Security"); fix_labels+=("Remove detected API keys/secrets from markdown files (+5)"); fix_sources+=("community: 'Never hardcode credentials' — mdskills.ai/docs/skill-best-practices"); }
[ "$x2" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Security"); fix_labels+=("Replace hardcoded paths (/Users/...) with relative paths (+3)"); fix_sources+=("spec: 'use relative paths from the skill root' — agentskills.io/specification#file-references"); }
[ "$x3" = "0" ] && { fix_count=$((fix_count+1)); fix_dims+=("Security"); fix_labels+=("Remove or guard dangerous commands (rm -rf, chmod 777) (+2)"); fix_sources+=("community: 'Shell commands need guardrails' — mdskills.ai/docs/skill-best-practices"); }
# ── Display: "How to Improve" section with sourced fixes (terminal only) ──
if [ "$OUTPUT_MODE" = "terminal" ]; then
if [ "$fix_count" -gt 0 ]; then
echo ""
echo ""
echo " $(_bold "┌────────────────────────────────────────────────────────┐")"
echo " $(_bold "│") $(_bold "HOW TO REACH 100/100") $(_dim "($fix_count items, by impact)") $(_bold "│")"
echo " $(_bold "└────────────────────────────────────────────────────────┘")"
local prev_dim=""
local i
for ((i=0; i<fix_count; i++)); do
local dim="${fix_dims[$i]}"
local label="${fix_labels[$i]}"
local source="${fix_sources[$i]}"
if [ "$dim" != "$prev_dim" ]; then
echo ""
echo " $(_bold_cyan "$dim:")"
prev_dim="$dim"
fi
echo " $(_yellow "→") $label"
echo " $(_dim "$source")"
done
echo ""
echo " $(_dim "Sources: spec = agentskills.io/specification")"
echo " $(_dim " official = agentskills.io/skill-creation/*")"
echo " $(_dim " community = mdskills.ai/docs/skill-best-practices")"
else
echo ""
_pass "Perfect score — no improvements needed"
fi
fi
}
# ════════════════════════════════════════════════════════════════════
# OUTPUT MODES
# ════════════════════════════════════════════════════════════════════
print_json() {
local elapsed=$(( SECONDS - SCAN_START ))
# Collect findings into JSON arrays
local errors_json="[]" warnings_json="[]" info_json="[]"
local err_items=""
while IFS='|' read -r sev sec msg; do
[ -z "$sev" ] && continue
msg=$(echo "$msg" | sed 's/"/\\"/g')
sec=$(echo "$sec" | sed 's/"/\\"/g')
case "$sev" in
ERROR) err_items="${err_items}{\"section\":\"$sec\",\"message\":\"$msg\"}," ;;
esac
done < "$FINDINGS_FILE"
local warn_items=""
while IFS='|' read -r sev sec msg; do
[ -z "$sev" ] && continue
msg=$(echo "$msg" | sed 's/"/\\"/g')
sec=$(echo "$sec" | sed 's/"/\\"/g')
case "$sev" in
WARN) warn_items="${warn_items}{\"section\":\"$sec\",\"message\":\"$msg\"}," ;;
esac
done < "$FINDINGS_FILE"
local info_items=""
while IFS='|' read -r sev sec msg; do
[ -z "$sev" ] && continue
msg=$(echo "$msg" | sed 's/"/\\"/g')
sec=$(echo "$sec" | sed 's/"/\\"/g')
case "$sev" in
INFO) info_items="${info_items}{\"section\":\"$sec\",\"message\":\"$msg\"}," ;;
esac
done < "$FINDINGS_FILE"
# Strip trailing commas and wrap
err_items="${err_items%,}"
warn_items="${warn_items%,}"
info_items="${info_items%,}"
[ -n "$err_items" ] && errors_json="[$err_items]"
[ -n "$warn_items" ] && warnings_json="[$warn_items]"
[ -n "$info_items" ] && info_json="[$info_items]"
local result="PASS"
[ "$WARNINGS" -gt 0 ] && result="PASS_WITH_WARNINGS"
[ "$ERRORS" -gt 0 ] && result="FAIL"
cat <<ENDJSON
{
"version": "4.0",
"skill": "$(pwd)",
"timestamp": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
"duration_seconds": $elapsed,
"result": "$result",
"counts": {
"checks": $CHECKS_RUN,
"errors": $ERRORS,
"warnings": $WARNINGS,
"info": $INFO_COUNT
},
"quality_score": {
"total": $QUALITY_SCORE,
"grade": "$QUALITY_GRADE",
"dimensions": {
"description_quality": { "score": $QUALITY_DESC, "max": 30 },
"instruction_clarity": { "score": $QUALITY_CLARITY, "max": 25 },
"spec_compliance": { "score": $QUALITY_SPEC, "max": 20 },
"progressive_disclosure": { "score": $QUALITY_PROGRESSIVE, "max": 15 },
"security": { "score": $QUALITY_SECURITY, "max": 10 }
}
},
"token_analysis": {
"skill_md_tokens": $_S_BODY_TOKENS,
"body_lines": $_S_BODY_LINES,
"code_blocks": $_S_CODE_BLOCKS,
"headings": $_S_HEADINGS,
"reference_links": $_S_REF_LINKS,
"reference_files_total": $_S_REFS_TOTAL,
"reference_files_linked": $_S_REFS_LINKED
},
"findings": {
"errors": $errors_json,
"warnings": $warnings_json,
"info": $info_json
}
}
ENDJSON
}
print_markdown() {
local elapsed=$(( SECONDS - SCAN_START ))
local result_icon result_text
if [ "$ERRORS" -gt 0 ]; then
result_icon="x" result_text="FAIL — $ERRORS error(s), $WARNINGS warning(s)"
elif [ "$WARNINGS" -gt 0 ]; then
result_icon="!" result_text="PASS with $WARNINGS warning(s)"
else
result_icon="+" result_text="All checks passed"
fi
cat <<ENDMD
# Agent Skill Scan Report
**Score: $QUALITY_SCORE/100 ($QUALITY_GRADE)** | Result: $result_text | Duration: ${elapsed}s
## Score Breakdown
| Dimension | Score | Max | % |
|---|---|---|---|
| Description Quality | $QUALITY_DESC | 30 | $(( (QUALITY_DESC * 100) / 30 ))% |
| Instruction Clarity | $QUALITY_CLARITY | 25 | $(( (QUALITY_CLARITY * 100) / 25 ))% |
| Spec Compliance | $QUALITY_SPEC | 20 | $(( (QUALITY_SPEC * 100) / 20 ))% |
| Progressive Disclosure | $QUALITY_PROGRESSIVE | 15 | $(( (QUALITY_PROGRESSIVE * 100) / 15 ))% |
| Security | $QUALITY_SECURITY | 10 | $(( (QUALITY_SECURITY * 100) / 10 ))% |
| **Total** | **$QUALITY_SCORE** | **100** | **${QUALITY_SCORE}%** |
## Summary
| Metric | Count |
|---|---|
| Sections checked | $CHECKS_RUN |
| Errors | $ERRORS |
| Warnings | $WARNINGS |
| Suggestions | $INFO_COUNT |
## Token Analysis
| File | Tokens | Lines |
|---|---|---|
| SKILL.md | ~$_S_BODY_TOKENS | $_S_BODY_LINES |
| Reference files | $_S_REFS_TOTAL files | $_S_REFS_LINKED linked |
ENDMD
# Findings
local total_findings
total_findings=$(wc -l < "$FINDINGS_FILE" | tr -d ' ')
if [ "$total_findings" -gt 0 ]; then
echo ""
echo "## Findings"
echo ""
local has_errors has_warnings has_info
has_errors=$(grep -c "^ERROR|" "$FINDINGS_FILE" 2>/dev/null || true)
has_warnings=$(grep -c "^WARN|" "$FINDINGS_FILE" 2>/dev/null || true)
has_info=$(grep -c "^INFO|" "$FINDINGS_FILE" 2>/dev/null || true)
if [ "${has_errors:-0}" -gt 0 ]; then
echo "### Errors"
echo ""
grep "^ERROR|" "$FINDINGS_FILE" | while IFS='|' read -r _ sec msg; do
echo "- **$sec**: $msg"
done
echo ""
fi
if [ "${has_warnings:-0}" -gt 0 ]; then
echo "### Warnings"
echo ""
grep "^WARN|" "$FINDINGS_FILE" | while IFS='|' read -r _ sec msg; do
echo "- **$sec**: $msg"
done
echo ""
fi
if [ "${has_info:-0}" -gt 0 ]; then
echo "### Suggestions"
echo ""
grep "^INFO|" "$FINDINGS_FILE" | while IFS='|' read -r _ sec msg; do
echo "- **$sec**: $msg"
done
echo ""
fi
fi
echo "---"
echo ""
echo "_Validated against [agentskills.io/specification](https://agentskills.io/specification) | Scanner v4.0 | $(date '+%Y-%m-%d %H:%M:%S')_"
}
print_score_only() {
if [ "$NO_COLOR" = "1" ]; then
echo "$QUALITY_SCORE/100 $QUALITY_GRADE"
else
echo "$QUALITY_SCORE/100 $(grade_color "$QUALITY_SCORE")"
fi
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
}
# ════════════════════════════════════════════════════════════════════
# REPORT (terminal)
# ════════════════════════════════════════════════════════════════════
print_report() {
echo ""
echo ""
local elapsed=$(( SECONDS - SCAN_START ))
_pad() {
local text="$1" width="$2"
local visible
visible=$(echo "$text" | sed 's/\x1b\[[0-9;]*m//g')
local pad_len=$(( width - ${#visible} ))
if [ "$pad_len" -lt 0 ]; then pad_len=0; fi
printf '%s%*s' "$text" "$pad_len" ""
}
local W=58
local line
line=$(printf '%.0s═' $(seq 1 $W))
local thin_line
thin_line=$(printf '%.0s─' $(seq 1 $W))
# ── Summary box ──
echo " $(_bold "${line}")"
echo " $(_bold "║")$(_pad "" $W)$(_bold "║")"
echo " $(_bold "║")$(_pad " AGENT SKILL SCAN REPORT " $W)$(_bold "║")"
echo " $(_bold "║")$(_pad " Scanner v4.0 " $W)$(_bold "║")"
echo " $(_bold "║")$(_pad "" $W)$(_bold "║")"
echo " $(_bold "${line}")"
echo "$(_pad "" $W)"
echo "$(_pad "Checks run : $CHECKS_RUN sections" $(( W - 2 )))"
echo "$(_pad "$(_red "Errors") : $ERRORS" $(( W - 2 )))"
echo "$(_pad "$(_yellow "Warnings") : $WARNINGS" $(( W - 2 )))"
echo "$(_pad "$(_blue "Info") : $INFO_COUNT" $(( W - 2 )))"
echo "$(_pad "Duration : ${elapsed}s" $(( W - 2 )))"
echo "$(_pad "" $W)"
echo " $(_bold "${line}")"
echo "$(_pad "" $W)"
local result_text
if [ "$ERRORS" -gt 0 ]; then
result_text=$(_bold_red "FAIL")
elif [ "$WARNINGS" -gt 0 ]; then
result_text=$(_bold_yellow "PASS with warnings")
else
result_text=$(_bold_green "ALL CLEAR")
fi
echo "$(_pad "Result : $result_text" $(( W - 2 )))"
echo "$(_pad "Quality Score : $QUALITY_SCORE/100 ($(grade_color "$QUALITY_SCORE"))" $(( W - 2 )))"
echo "$(_pad "" $W)"
echo " $(_bold "${line}")"
echo "$(_pad "" $W)"
echo "$(_pad "Description : $QUALITY_DESC/30" $(( W - 2 )))"
echo "$(_pad "Clarity : $QUALITY_CLARITY/25" $(( W - 2 )))"
echo "$(_pad "Spec : $QUALITY_SPEC/20" $(( W - 2 )))"
echo "$(_pad "Progressive : $QUALITY_PROGRESSIVE/15" $(( W - 2 )))"
echo "$(_pad "Security : $QUALITY_SECURITY/10" $(( W - 2 )))"
echo "$(_pad "" $W)"
echo " $(_bold "${line}")"
# ── Detailed findings ──
local total_findings
total_findings=$(wc -l < "$FINDINGS_FILE" | tr -d ' ')
if [ "$total_findings" -gt 0 ]; then
echo ""
echo " $(_bold "${thin_line}")"
echo " $(_bold "│")$(_pad " DETAILED FINDINGS " $W)$(_bold "│")"
echo " $(_bold "${thin_line}")"
_print_findings_by_severity() {
local severity="$1" label="$2" color_fn="$3"
local matches
matches=$(grep "^${severity}|" "$FINDINGS_FILE" 2>/dev/null || true)
[ -z "$matches" ] && return
local count
count=$(echo "$matches" | wc -l | tr -d ' ')
echo ""
echo " $($color_fn "$label ($count)")"
echo " $(_dim "$(printf '%.0s─' $(seq 1 56))")"
local prev_section=""
while IFS='|' read -r _ sec msg; do
if [ "$sec" != "$prev_section" ]; then
echo ""
echo " $(_dim "[$sec]")"
prev_section="$sec"
fi
echo " $($color_fn "▸") $msg"
done <<< "$matches"
}
_print_findings_by_severity "ERROR" "ERRORS" "_red"
_print_findings_by_severity "WARN" "WARNINGS" "_yellow"
_print_findings_by_severity "INFO" "SUGGESTIONS" "_blue"
echo ""
echo " $(_dim "$(printf '%.0s─' $(seq 1 60))")"
else
echo ""
echo " $(_bold_green "No findings — skill package is in perfect shape.")"
fi
echo ""
echo " $(_dim "Validated against agentskills.io/specification")"
echo " $(_dim "Scored using skill-tools 5-dimension rubric (0-100)")"
echo ""
# ── CI job summary ──
if [ "$CI" = "true" ]; then
{
echo "## Agent Skill Scan Report"
echo ""
if [ "$ERRORS" -gt 0 ]; then
echo "**FAIL** — $ERRORS error(s), $WARNINGS warning(s)"
elif [ "$WARNINGS" -gt 0 ]; then
echo "**PASS with $WARNINGS warning(s)**"
else
echo "**All checks passed**"
fi
echo ""
echo "**Quality Score: $QUALITY_SCORE/100 (Grade: $QUALITY_GRADE)**"
echo ""
echo "| Metric | Count |"
echo "|--------|-------|"
echo "| Checks | $CHECKS_RUN |"
echo "| Errors | $ERRORS |"
echo "| Warnings | $WARNINGS |"
echo "| Info | $INFO_COUNT |"
echo "| Duration | ${elapsed}s |"
echo ""
echo "| Dimension | Score |"
echo "|-----------|-------|"
echo "| Description Quality | $QUALITY_DESC/30 |"
echo "| Instruction Clarity | $QUALITY_CLARITY/25 |"
echo "| Spec Compliance | $QUALITY_SPEC/20 |"
echo "| Progressive Disclosure | $QUALITY_PROGRESSIVE/15 |"
echo "| Security | $QUALITY_SECURITY/10 |"
echo "| **Total** | **$QUALITY_SCORE/100 ($QUALITY_GRADE)** |"
if [ "$total_findings" -gt 0 ]; then
echo ""
echo "### Findings"
echo ""
echo "<details>"
echo "<summary>Click to expand ($total_findings findings)</summary>"
echo ""
_ci_findings() {
local severity="$1" icon="$2"
local matches
matches=$(grep "^${severity}|" "$FINDINGS_FILE" 2>/dev/null || true)
[ -z "$matches" ] && return
while IFS='|' read -r _ sec msg; do
echo "- ${icon} **${sec}**: ${msg}"
done <<< "$matches"
}
_ci_findings "ERROR" "x"
_ci_findings "WARN" "!"
_ci_findings "INFO" "i"
echo ""
echo "</details>"
fi
echo ""
echo "_Validated against [agentskills.io/specification](https://agentskills.io/specification) | Score: $QUALITY_SCORE/100 ($QUALITY_GRADE)_"
} >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
fi
}
# ════════════════════════════════════════════════════════════════════
# MAIN
# ════════════════════════════════════════════════════════════════════
show_help() {
echo ""
echo " $(_bold "Agent Skill Scanner v4")"
echo ""
echo " Validates skill packages against the agentskills.io specification"
echo " and community best practices for AI agent skills."
echo " Computes a quality score (0-100) across 5 dimensions."
echo ""
echo " $(_bold "Usage:")"
echo " ./scripts/validate.sh Run full scan (terminal)"
echo " ./scripts/validate.sh --json Output as JSON"
echo " ./scripts/validate.sh --md Output as Markdown report"
echo " ./scripts/validate.sh --score-only Print score and grade only"
echo " ./scripts/validate.sh --help Show this help"
echo ""
echo " $(_bold "Output Modes:")"
echo " $(_cyan "--json") Machine-readable JSON (pipe to jq, feed to web tools)"
echo " $(_cyan "--md") Markdown report (save to file, paste in PRs/docs)"
echo " $(_cyan "--score-only") Quick score check (e.g., in pre-commit hooks)"
echo " $(_dim "(default)") Rich terminal output with colors and bar charts"
echo ""
echo " $(_bold "Environment:")"
echo " CI=true Emit GitHub Actions annotations + job summary"
echo " NO_COLOR=1 Disable colored output"
echo ""
echo " $(_bold "Checks (15 sections, 20+ individual checks):")"
echo " $(_cyan " 1.") Skill structure Required/optional files and directories"
echo " $(_cyan " 2.") Frontmatter & desc name, description, voice, verbs, negative triggers"
echo " $(_cyan " 3.") Body & disclosure Line count, token budget, structure, code examples"
echo " $(_cyan " 4.") Internal links All links resolve to existing files"
echo " $(_cyan " 5.") Reference files Linked, orphaned, empty, and missing detection"
echo " $(_cyan " 6.") Markdown syntax Unclosed code blocks"
echo " $(_cyan " 7.") Reference nesting No deep cross-reference chains"
echo " $(_cyan " 8.") Scripts Executable permissions, documentation"
echo " $(_cyan " 9.") Repository hygiene Sensitive files, development artifacts"
echo " $(_cyan "10.") Token budget Per-file and total token analysis with zones"
echo " $(_cyan "11.") Content quality Code examples, headings, keyword density"
echo " $(_cyan "12.") Agent metadata agents/openai.yaml validation"
echo " $(_cyan "13.") Security scan API keys, secrets, hardcoded paths, dangerous commands"
echo " $(_cyan "14.") Heading hierarchy Level skip detection (MD001), duplicate headings (MD024)"
echo " $(_cyan "15.") Quality score 0-100 score across 5 dimensions with letter grade"
echo ""
echo " $(_bold "Quality Score Dimensions:")"
echo " Description Quality (30 pts) Length, specificity, voice, verbs, triggers"
echo " Instruction Clarity (25 pts) Code blocks, headings, reference links"
echo " Spec Compliance (20 pts) Required fields, name format, token/line limits"
echo " Progressive Discl. (15 pts) References dir, linking, body size"
echo " Security (10 pts) No secrets, paths, or dangerous commands"
echo ""
echo " $(_bold "Letter Grades:")"
echo " $(_bold_green "A+") 95+ $(_bold_green "A") 90+ $(_bold_green "A-") 85+ $(_bold_yellow "B+") 80+ $(_bold_yellow "B") 75+"
echo " $(_bold_yellow "B-") 70+ $(_bold_yellow "C+") 65+ $(_bold_red "C") 60+ $(_bold_red "C-") 55+ $(_bold_red "D") 50+ $(_bold_red "F") <50"
echo ""
echo " $(_bold "Token Zones:")"
echo " $(_green "[SAFE]") Well within budget, efficient"
echo " $(_yellow "[WARN]") At or near recommended limits"
echo " $(_red "[HIGH]") Exceeding recommendations, consider optimization"
echo ""
echo " $(_bold "Token Estimation:")"
echo " Uses chars/4 approximation (industry standard for tiktoken cl100k_base)"
echo " SKILL.md budget: <5000 tokens | Reference: <4000 tokens each"
echo " Total package: <50000 tokens recommended"
echo ""
echo " $(_bold "Severity:")"
echo " $(_red "✗ ERROR") Spec violation or broken content — must fix"
echo " $(_yellow "! WARN ") Best practice issue — should fix"
echo " $(_blue " INFO ") Suggestion — nice to have"
echo " $(_green "✓ PASS ") Check passed"
echo ""
echo " $(_bold "References:")"
echo " Spec: https://agentskills.io/specification"
echo " Scoring: https://agentskills.io/skill-creation/best-practices"
echo " Tokens: https://agentpatterns.ai/context-engineering/context-budget-allocation/"
echo ""
}
_suppress_terminal() {
[ "$OUTPUT_MODE" != "terminal" ]
}
main() {
# Parse CLI flags
while [ $# -gt 0 ]; do
case "$1" in
--help|-h) show_help; exit 0 ;;
--json) OUTPUT_MODE="json"; NO_COLOR=1 ;;
--md|--markdown) OUTPUT_MODE="markdown"; NO_COLOR=1 ;;
--score-only|--score) OUTPUT_MODE="score-only" ;;
*) echo "Unknown flag: $1"; echo "Run with --help for usage."; exit 2 ;;
esac
shift
done
if ! _suppress_terminal; then
echo ""
echo " $(_bold "╔══════════════════════════════════════════════════════════╗")"
echo " $(_bold "║") $(_bold "║")"
echo " $(_bold "║") $(_bold_cyan "A G E N T S K I L L S C A N N E R") $(_bold "║")"
echo " $(_bold "║") $(_dim "v 4 . 0") $(_bold "║")"
echo " $(_bold "║") $(_bold "║")"
echo " $(_bold "╚══════════════════════════════════════════════════════════╝")"
echo ""
if [ "$CI" = "true" ]; then
_detail "Mode : CI (GitHub Actions annotations + job summary)"
else
_detail "Mode : Local"
fi
_detail "Skill: $(pwd)"
_detail "Spec : agentskills.io/specification"
_detail "Date : $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
echo " $(_dim "Running $TOTAL_CHECKS sections with 20+ individual checks...")"
fi
if [ "$OUTPUT_MODE" != "terminal" ]; then
exec 3>&1 1>/dev/null
fi
check_structure
check_frontmatter
check_body
check_links
check_references
check_markdown
check_reference_depth
check_scripts
check_repo_hygiene
check_token_budget
check_content_quality
check_agents_metadata
check_security
check_heading_hierarchy
compute_quality_score
if [ "$OUTPUT_MODE" != "terminal" ]; then
exec 1>&3 3>&-
fi
case "$OUTPUT_MODE" in
json) print_json ;;
markdown) print_markdown ;;
score-only) print_score_only ;;
*) print_report ;;
esac
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
}
main "$@"