validate.sh 85 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
  4. # โ•‘ Agent Skill Scanner v4 โ•‘
  5. # โ•‘ Validates skill packages against the agentskills.io spec โ•‘
  6. # โ•‘ and best practices. Runs locally and in CI (GitHub Actions). โ•‘
  7. # โ•‘ โ•‘
  8. # โ•‘ Spec: https://agentskills.io/specification โ•‘
  9. # โ•‘ Guide: https://agentskills.io/skill-creation โ•‘
  10. # โ•‘ โ•‘
  11. # โ•‘ Token Estimation: โ•‘
  12. # โ•‘ - Uses character/4 approximation (industry standard) โ•‘
  13. # โ•‘ - Accurate within ~10% for English text โ•‘
  14. # โ•‘ - Based on OpenAI tiktoken cl100k_base encoding โ•‘
  15. # โ•‘ โ•‘
  16. # โ•‘ Quality Score: 0-100 across 5 dimensions โ•‘
  17. # โ•‘ - Description Quality (30) ยท Spec Compliance (20) โ•‘
  18. # โ•‘ - Instruction Clarity (25) ยท Progressive Disclosure (15) โ•‘
  19. # โ•‘ - Security (10) โ•‘
  20. # โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  21. #
  22. # Usage:
  23. # ./scripts/validate.sh # full scan
  24. # ./scripts/validate.sh --help # show usage
  25. #
  26. # Environment:
  27. # CI=true โ€” emits GitHub Actions annotations (auto-detected)
  28. # NO_COLOR=1 โ€” disable colored output
  29. SCAN_START=$SECONDS
  30. ERRORS=0
  31. WARNINGS=0
  32. INFO_COUNT=0
  33. CHECKS_RUN=0
  34. TOTAL_CHECKS=15
  35. CURRENT_SECTION=""
  36. CI="${CI:-false}"
  37. NO_COLOR="${NO_COLOR:-0}"
  38. OUTPUT_MODE="terminal"
  39. SKILL_FILE="SKILL.md"
  40. SKILL_DIR="$(basename "$(pwd)")"
  41. FINDINGS_FILE=$(mktemp)
  42. trap 'rm -f "$FINDINGS_FILE"' EXIT
  43. # โ”€โ”€ Spec limits (agentskills.io/specification) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  44. NAME_MAX_LEN=64
  45. DESC_MAX_LEN=1024
  46. DESC_MIN_USEFUL=30
  47. BODY_MAX_LINES=500
  48. TOKEN_BUDGET=5000
  49. COMPAT_MAX_LEN=500
  50. # โ”€โ”€ Token budget zones โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  51. TOKEN_SAFE_ZONE=3500
  52. TOKEN_WARN_ZONE=5000
  53. TOKEN_DANGER_ZONE=8000
  54. REF_TOKEN_SAFE=2000
  55. REF_TOKEN_WARN=3500
  56. REF_TOKEN_DANGER=4500
  57. TOTAL_TOKEN_SAFE=50000
  58. TOTAL_TOKEN_WARN=100000
  59. TOTAL_TOKEN_DANGER=200000
  60. # โ”€โ”€ Score trackers (populated during checks, consumed by scoring) โ”€โ”€
  61. _S_HAS_NAME=false
  62. _S_NAME_OK=false
  63. _S_HAS_DESC=false
  64. _S_DESC_LEN=0
  65. _S_DESC_WHAT=false
  66. _S_DESC_WHEN=false
  67. _S_DESC_FIRST_PERSON=false
  68. _S_DESC_GENERIC=false
  69. _S_DESC_NEGATIVES=false
  70. _S_CODE_BLOCKS=0
  71. _S_HEADINGS=0
  72. _S_REF_LINKS=0
  73. _S_BODY_LINES=0
  74. _S_BODY_TOKENS=0
  75. _S_HAS_REFS_DIR=false
  76. _S_REFS_LINKED=0
  77. _S_REFS_TOTAL=0
  78. _S_FENCES_OK=true
  79. _S_SECRETS=0
  80. _S_HARDCODED=0
  81. _S_DANGEROUS=0
  82. QUALITY_SCORE=0
  83. QUALITY_GRADE="F"
  84. QUALITY_DESC=0
  85. QUALITY_CLARITY=0
  86. QUALITY_SPEC=0
  87. QUALITY_PROGRESSIVE=0
  88. QUALITY_SECURITY=0
  89. # โ”€โ”€ Colors & formatting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  90. if [ "$NO_COLOR" = "1" ]; then
  91. _red() { printf '%s' "$*"; }
  92. _green() { printf '%s' "$*"; }
  93. _yellow() { printf '%s' "$*"; }
  94. _blue() { printf '%s' "$*"; }
  95. _cyan() { printf '%s' "$*"; }
  96. _magenta() { printf '%s' "$*"; }
  97. _bold() { printf '%s' "$*"; }
  98. _dim() { printf '%s' "$*"; }
  99. _bold_green() { printf '%s' "$*"; }
  100. _bold_red() { printf '%s' "$*"; }
  101. _bold_yellow() { printf '%s' "$*"; }
  102. _bold_cyan() { printf '%s' "$*"; }
  103. else
  104. _red() { printf "\033[0;31m%s\033[0m" "$*"; }
  105. _green() { printf "\033[0;32m%s\033[0m" "$*"; }
  106. _yellow() { printf "\033[0;33m%s\033[0m" "$*"; }
  107. _blue() { printf "\033[0;34m%s\033[0m" "$*"; }
  108. _cyan() { printf "\033[0;36m%s\033[0m" "$*"; }
  109. _magenta() { printf "\033[0;35m%s\033[0m" "$*"; }
  110. _bold() { printf "\033[1m%s\033[0m" "$*"; }
  111. _dim() { printf "\033[2m%s\033[0m" "$*"; }
  112. _bold_green() { printf "\033[1;32m%s\033[0m" "$*"; }
  113. _bold_red() { printf "\033[1;31m%s\033[0m" "$*"; }
  114. _bold_yellow() { printf "\033[1;33m%s\033[0m" "$*"; }
  115. _bold_cyan() { printf "\033[1;36m%s\033[0m" "$*"; }
  116. fi
  117. # โ”€โ”€ Logging โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  118. _error() {
  119. ERRORS=$((ERRORS + 1))
  120. [ "$OUTPUT_MODE" = "terminal" ] && echo " $(_red "โœ— ERROR") $*" || true
  121. echo "ERROR|${CURRENT_SECTION}|$*" >> "$FINDINGS_FILE"
  122. }
  123. _warn() {
  124. WARNINGS=$((WARNINGS + 1))
  125. [ "$OUTPUT_MODE" = "terminal" ] && echo " $(_yellow "! WARN ") $*" || true
  126. echo "WARN|${CURRENT_SECTION}|$*" >> "$FINDINGS_FILE"
  127. }
  128. _pass() {
  129. [ "$OUTPUT_MODE" = "terminal" ] && echo " $(_green "โœ“ PASS ") $*" || true
  130. }
  131. _info() {
  132. INFO_COUNT=$((INFO_COUNT + 1))
  133. [ "$OUTPUT_MODE" = "terminal" ] && echo " $(_blue "โ„น INFO ") $*" || true
  134. echo "INFO|${CURRENT_SECTION}|$*" >> "$FINDINGS_FILE"
  135. }
  136. _detail() {
  137. [ "$OUTPUT_MODE" = "terminal" ] && echo " $(_dim "$*")" || true
  138. }
  139. ci_annotate() {
  140. local level="$1"; shift
  141. if [ "$CI" = "true" ]; then
  142. echo "::${level} $*"
  143. fi
  144. }
  145. section() {
  146. CHECKS_RUN=$((CHECKS_RUN + 1))
  147. CURRENT_SECTION="$1"
  148. if [ "$OUTPUT_MODE" = "terminal" ]; then
  149. echo ""
  150. echo " $(_bold_cyan "[$CHECKS_RUN/$TOTAL_CHECKS]") $(_bold "$1")"
  151. echo " $(_dim "$(printf '%.0sโ”€' $(seq 1 60))")"
  152. fi
  153. }
  154. file_lines() { wc -l < "$1" | tr -d ' '; }
  155. file_chars() { wc -c < "$1" | tr -d ' '; }
  156. file_words() { wc -w < "$1" | tr -d ' '; }
  157. estimate_tokens() {
  158. local chars
  159. chars=$(file_chars "$1")
  160. echo $(( chars / 4 ))
  161. }
  162. estimate_tokens_detailed() {
  163. local file="$1"
  164. local chars words lines char_tokens word_tokens
  165. chars=$(file_chars "$file")
  166. words=$(file_words "$file")
  167. lines=$(file_lines "$file")
  168. char_tokens=$(( chars / 4 ))
  169. word_tokens=$(( (words * 13) / 10 ))
  170. echo "$char_tokens|$chars|$words|$lines|$word_tokens"
  171. }
  172. token_zone_color() {
  173. local tokens="$1" safe="$2" warn="$3"
  174. if [ "$tokens" -le "$safe" ]; then
  175. echo "green"
  176. elif [ "$tokens" -le "$warn" ]; then
  177. echo "yellow"
  178. else
  179. echo "red"
  180. fi
  181. }
  182. format_tokens_colored() {
  183. local tokens="$1" safe="$2" warn="$3" label="${4:-tokens}"
  184. local zone
  185. zone=$(token_zone_color "$tokens" "$safe" "$warn")
  186. case "$zone" in
  187. green) printf "%s" "$(_green "~$tokens $label")" ;;
  188. yellow) printf "%s" "$(_yellow "~$tokens $label")" ;;
  189. red) printf "%s" "$(_red "~$tokens $label")" ;;
  190. esac
  191. }
  192. token_zone_indicator() {
  193. local tokens="$1" safe="$2" warn="$3"
  194. local zone pct
  195. zone=$(token_zone_color "$tokens" "$safe" "$warn")
  196. pct=$(( (tokens * 100) / warn ))
  197. case "$zone" in
  198. green)
  199. if [ "$NO_COLOR" = "1" ]; then echo "[SAFE $pct%]"
  200. else echo "$(_green "[SAFE $pct%]")"; fi ;;
  201. yellow)
  202. if [ "$NO_COLOR" = "1" ]; then echo "[WARN $pct%]"
  203. else echo "$(_yellow "[WARN $pct%]")"; fi ;;
  204. red)
  205. if [ "$NO_COLOR" = "1" ]; then echo "[HIGH $pct%]"
  206. else echo "$(_red "[HIGH $pct%]")"; fi ;;
  207. esac
  208. }
  209. # โ”€โ”€ Visual helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  210. bar_gauge() {
  211. local value="$1" max="$2" width="${3:-20}"
  212. local pct filled empty bar=""
  213. if [ "$max" -le 0 ]; then pct=0
  214. else pct=$(( (value * 100) / max )); fi
  215. [ "$pct" -gt 100 ] && pct=100
  216. filled=$(( (pct * width) / 100 ))
  217. empty=$(( width - filled ))
  218. local i
  219. for ((i=0; i<filled; i++)); do bar+="โ–ˆ"; done
  220. for ((i=0; i<empty; i++)); do bar+="โ–‘"; done
  221. echo "$bar"
  222. }
  223. bar_gauge_colored() {
  224. local value="$1" max="$2" width="${3:-20}" safe="${4:-0}" warn="${5:-0}"
  225. local pct gauge
  226. if [ "$max" -le 0 ]; then pct=0
  227. else pct=$(( (value * 100) / max )); fi
  228. [ "$pct" -gt 100 ] && pct=100
  229. gauge=$(bar_gauge "$value" "$max" "$width")
  230. if [ "$safe" -gt 0 ] && [ "$warn" -gt 0 ]; then
  231. local zone
  232. zone=$(token_zone_color "$value" "$safe" "$warn")
  233. case "$zone" in
  234. green) printf "%s %s" "$(_green "$gauge")" "$(_green "${pct}%")" ;;
  235. yellow) printf "%s %s" "$(_yellow "$gauge")" "$(_yellow "${pct}%")" ;;
  236. red) printf "%s %s" "$(_red "$gauge")" "$(_red "${pct}%")" ;;
  237. esac
  238. else
  239. printf "%s %d%%" "$gauge" "$pct"
  240. fi
  241. }
  242. letter_grade() {
  243. local score="$1"
  244. if [ "$score" -ge 95 ]; then echo "A+"
  245. elif [ "$score" -ge 90 ]; then echo "A"
  246. elif [ "$score" -ge 85 ]; then echo "A-"
  247. elif [ "$score" -ge 80 ]; then echo "B+"
  248. elif [ "$score" -ge 75 ]; then echo "B"
  249. elif [ "$score" -ge 70 ]; then echo "B-"
  250. elif [ "$score" -ge 65 ]; then echo "C+"
  251. elif [ "$score" -ge 60 ]; then echo "C"
  252. elif [ "$score" -ge 55 ]; then echo "C-"
  253. elif [ "$score" -ge 50 ]; then echo "D"
  254. else echo "F"
  255. fi
  256. }
  257. grade_color() {
  258. local score="$1" grade
  259. grade=$(letter_grade "$score")
  260. if [ "$score" -ge 85 ]; then printf "%s" "$(_bold_green "$grade")"
  261. elif [ "$score" -ge 65 ]; then printf "%s" "$(_bold_yellow "$grade")"
  262. else printf "%s" "$(_bold_red "$grade")"
  263. fi
  264. }
  265. safe_count() {
  266. local result
  267. result=$("$@" 2>/dev/null | tr -d ' ' || true)
  268. if [ -z "$result" ] || ! [[ "$result" =~ ^[0-9]+$ ]]; then
  269. echo "0"
  270. else
  271. echo "$result"
  272. fi
  273. }
  274. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  275. # CHECKS
  276. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  277. # โ”€โ”€ [1] Skill Structure โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  278. check_structure() {
  279. section "Skill Structure"
  280. if [ ! -f "$SKILL_FILE" ]; then
  281. _error "SKILL.md not found โ€” required by agentskills.io spec"
  282. ci_annotate "error" "file=SKILL.md::SKILL.md not found"
  283. return
  284. fi
  285. _pass "SKILL.md ($(file_lines "$SKILL_FILE") lines, $(file_chars "$SKILL_FILE") bytes)"
  286. if [ ! -f "README.md" ]; then
  287. _warn "README.md missing โ€” recommended for discoverability and GitHub rendering"
  288. ci_annotate "warning" "file=README.md::README.md missing (recommended)"
  289. else
  290. _pass "README.md ($(file_lines README.md) lines)"
  291. fi
  292. local dirs=("references" "scripts" "assets" "agents")
  293. for dir in "${dirs[@]}"; do
  294. if [ -d "$dir" ]; then
  295. local count
  296. count=$(find "$dir" -type f | wc -l | tr -d ' ')
  297. _pass "$dir/ ($count files)"
  298. if [ "$dir" = "references" ]; then
  299. _S_HAS_REFS_DIR=true
  300. fi
  301. fi
  302. done
  303. if [ ! -d "references" ]; then
  304. _info "No references/ directory"
  305. _detail "Add reference docs for progressive disclosure of complex content"
  306. fi
  307. if [ -f "LICENSE" ] || [ -f "LICENSE.md" ] || [ -f "LICENSE.txt" ]; then
  308. _pass "License file found"
  309. fi
  310. }
  311. # โ”€โ”€ [2] Frontmatter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  312. check_frontmatter() {
  313. [ ! -f "$SKILL_FILE" ] && return
  314. section "Frontmatter & Description Quality"
  315. local first_line
  316. first_line=$(head -1 "$SKILL_FILE")
  317. if [ "$first_line" != "---" ]; then
  318. _error "Line 1: Expected '---' delimiter, found: '$first_line'"
  319. ci_annotate "error" "file=$SKILL_FILE,line=1::Missing YAML frontmatter delimiter"
  320. return
  321. fi
  322. local frontmatter
  323. frontmatter=$(sed -n '2,/^---$/p' "$SKILL_FILE" | sed '$d')
  324. # โ”€โ”€ name โ”€โ”€
  325. local name
  326. name=$(echo "$frontmatter" | grep '^name:' | head -1 | sed 's/^name:[[:space:]]*//')
  327. if [ -z "$name" ]; then
  328. _error "Required field 'name' missing"
  329. ci_annotate "error" "file=$SKILL_FILE::Missing required 'name' field"
  330. else
  331. _S_HAS_NAME=true
  332. local name_len=${#name}
  333. local name_ok=true
  334. if [ "$name_len" -gt "$NAME_MAX_LEN" ]; then
  335. _error "name '$name' is $name_len chars (max $NAME_MAX_LEN)"
  336. ci_annotate "error" "file=$SKILL_FILE::name exceeds $NAME_MAX_LEN chars"
  337. name_ok=false
  338. fi
  339. if echo "$name" | grep -qE '[A-Z]'; then
  340. _error "name '$name' has uppercase โ€” spec requires lowercase only"
  341. ci_annotate "error" "file=$SKILL_FILE::name must be lowercase"
  342. name_ok=false
  343. fi
  344. if echo "$name" | grep -qE '[^a-z0-9-]'; then
  345. _error "name '$name' has invalid chars โ€” only a-z, 0-9, hyphens allowed"
  346. ci_annotate "error" "file=$SKILL_FILE::name contains invalid characters"
  347. name_ok=false
  348. fi
  349. if echo "$name" | grep -qE '^-|-$'; then
  350. _error "name '$name' starts or ends with hyphen"
  351. ci_annotate "error" "file=$SKILL_FILE::name starts/ends with hyphen"
  352. name_ok=false
  353. fi
  354. if echo "$name" | grep -qF -- '--'; then
  355. _error "name '$name' has consecutive hyphens (--)"
  356. ci_annotate "error" "file=$SKILL_FILE::name has consecutive hyphens"
  357. name_ok=false
  358. fi
  359. if [ "$name_ok" = true ]; then
  360. _pass "name: '$name' ($name_len chars)"
  361. _S_NAME_OK=true
  362. fi
  363. if [ "$name" != "$SKILL_DIR" ]; then
  364. _warn "name '$name' โ‰  directory '$SKILL_DIR'"
  365. _detail "Spec: name should match parent directory name"
  366. ci_annotate "warning" "file=$SKILL_FILE::name doesn't match directory name"
  367. else
  368. _pass "name matches directory name"
  369. fi
  370. fi
  371. # โ”€โ”€ description โ”€โ”€
  372. local desc_line desc=""
  373. desc_line=$(echo "$frontmatter" | grep -n '^description:' | head -1 | cut -d: -f1)
  374. if [ -z "$desc_line" ]; then
  375. _error "Required field 'description' missing"
  376. ci_annotate "error" "file=$SKILL_FILE::Missing required 'description' field"
  377. else
  378. _S_HAS_DESC=true
  379. local inline_desc
  380. inline_desc=$(echo "$frontmatter" | sed -n "${desc_line}p" | sed 's/^description:[[:space:]]*//')
  381. if [ -n "$inline_desc" ] && ! echo "$inline_desc" | grep -qE '^\s*[>|]\s*$'; then
  382. desc="$inline_desc"
  383. else
  384. desc=$(echo "$frontmatter" | sed -n "$((desc_line+1)),\$p" | sed '/^[a-zA-Z_-]*:/,$d' | tr '\n' ' ' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
  385. fi
  386. local desc_len=${#desc}
  387. _S_DESC_LEN=$desc_len
  388. if [ "$desc_len" -eq 0 ]; then
  389. _error "description is empty"
  390. ci_annotate "error" "file=$SKILL_FILE::description is empty"
  391. else
  392. if [ "$desc_len" -gt "$DESC_MAX_LEN" ]; then
  393. _warn "description is $desc_len chars (spec max: $DESC_MAX_LEN)"
  394. ci_annotate "warning" "file=$SKILL_FILE::description exceeds $DESC_MAX_LEN chars"
  395. else
  396. _pass "description length: $desc_len chars (limit: $DESC_MAX_LEN)"
  397. fi
  398. if [ "$desc_len" -lt "$DESC_MIN_USEFUL" ]; then
  399. _warn "description is very short ($desc_len chars) โ€” likely won't trigger well"
  400. _detail "Good: 'Extract text and tables from PDF files, fill forms, merge"
  401. _detail " documents. Use when working with PDF documents.'"
  402. _detail "Bad: 'Helps with PDFs.'"
  403. ci_annotate "warning" "file=$SKILL_FILE::description too short for reliable triggering"
  404. fi
  405. # WHAT and WHEN analysis
  406. local has_what=false has_when=false
  407. 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
  408. has_what=true
  409. _S_DESC_WHAT=true
  410. fi
  411. 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
  412. has_when=true
  413. _S_DESC_WHEN=true
  414. fi
  415. if [ "$has_what" = true ] && [ "$has_when" = true ]; then
  416. _pass "description covers WHAT and WHEN to use"
  417. elif [ "$has_what" = false ] && [ "$has_when" = false ]; then
  418. _warn "description may lack WHAT the skill does and WHEN to use it"
  419. _detail "Spec: 'Describes what the skill does and when to use it'"
  420. elif [ "$has_when" = false ]; then
  421. _info "description explains WHAT but could clarify WHEN to trigger"
  422. _detail "Adding 'Use when...' helps agents decide when to activate"
  423. fi
  424. # โ”€โ”€ [NEW] First-person voice โ”€โ”€
  425. if echo "$desc" | grep -qiE '\bI can\b|\bI will\b|\bI help\b|\bI am\b|\bI provide\b|\bmy skill\b'; then
  426. _warn "description uses first-person voice ('I can', 'I will', etc.)"
  427. _detail "Descriptions are injected into the agent system prompt โ€” first-person"
  428. _detail "mixes viewpoints and confuses agent reasoning. Use third person instead"
  429. ci_annotate "warning" "file=$SKILL_FILE::description uses first-person voice"
  430. _S_DESC_FIRST_PERSON=true
  431. else
  432. _pass "description uses correct voice (not first-person)"
  433. fi
  434. # โ”€โ”€ [NEW] Generic verb detection โ”€โ”€
  435. local generic_matches
  436. 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)
  437. if [ -n "$generic_matches" ]; then
  438. local generic_list
  439. generic_list=$(echo "$generic_matches" | tr '\n' ', ' | sed 's/,$//')
  440. _info "description contains generic verbs: $generic_list"
  441. _detail "Specific verbs (extract, generate, validate) improve trigger precision"
  442. _S_DESC_GENERIC=true
  443. else
  444. _pass "description uses specific action verbs"
  445. fi
  446. # โ”€โ”€ [NEW] Negative trigger detection โ”€โ”€
  447. if echo "$desc" | grep -qiE 'not for|not designed for|do not use|does not|don.t use'; then
  448. _pass "description includes boundary markers (negative triggers)"
  449. _S_DESC_NEGATIVES=true
  450. else
  451. _info "no negative triggers ('NOT for...', 'Do not use when...')"
  452. _detail "Negative triggers prevent mis-activation when many skills are loaded"
  453. fi
  454. fi
  455. fi
  456. # โ”€โ”€ optional fields โ”€โ”€
  457. echo ""
  458. _detail "Optional fields:"
  459. if echo "$frontmatter" | grep -q '^license:'; then
  460. local license_val
  461. license_val=$(echo "$frontmatter" | grep '^license:' | sed 's/^license:[[:space:]]*//')
  462. _pass "license: $license_val"
  463. else
  464. _info "No license field โ€” recommended for shared/public skills"
  465. fi
  466. if echo "$frontmatter" | grep -q '^compatibility:'; then
  467. local compat
  468. compat=$(echo "$frontmatter" | grep '^compatibility:' | sed 's/^compatibility:[[:space:]]*//')
  469. local compat_len=${#compat}
  470. if [ "$compat_len" -gt "$COMPAT_MAX_LEN" ]; then
  471. _warn "compatibility is $compat_len chars (spec max: $COMPAT_MAX_LEN)"
  472. else
  473. _pass "compatibility field present ($compat_len chars)"
  474. fi
  475. fi
  476. if echo "$frontmatter" | grep -q '^metadata:'; then
  477. _pass "metadata field present"
  478. if echo "$frontmatter" | grep -q '^\s*version:'; then
  479. _pass "metadata.version set"
  480. fi
  481. if echo "$frontmatter" | grep -q '^\s*author:'; then
  482. _pass "metadata.author set"
  483. fi
  484. fi
  485. if echo "$frontmatter" | grep -q '^allowed-tools:'; then
  486. _pass "allowed-tools field present (experimental)"
  487. fi
  488. }
  489. # โ”€โ”€ [3] Body Content & Progressive Disclosure โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  490. check_body() {
  491. [ ! -f "$SKILL_FILE" ] && return
  492. section "Body Content & Progressive Disclosure"
  493. local body_start
  494. body_start=$(grep -n '^---$' "$SKILL_FILE" | sed -n '2p' | cut -d: -f1)
  495. if [ -z "$body_start" ]; then
  496. _error "No closing frontmatter delimiter (---) found"
  497. ci_annotate "error" "file=$SKILL_FILE::Missing closing frontmatter delimiter"
  498. return
  499. fi
  500. local body_lines token_est
  501. body_lines=$(tail -n +"$((body_start + 1))" "$SKILL_FILE" | wc -l | tr -d ' ')
  502. token_est=$(estimate_tokens "$SKILL_FILE")
  503. _S_BODY_LINES=$body_lines
  504. _S_BODY_TOKENS=$token_est
  505. # Line count with bar gauge
  506. if [ "$body_lines" -eq 0 ]; then
  507. _error "SKILL.md body is empty โ€” agents need instructions"
  508. ci_annotate "error" "file=$SKILL_FILE::Empty body"
  509. elif [ "$body_lines" -gt "$BODY_MAX_LINES" ]; then
  510. _warn "Body: $body_lines lines (spec recommends <$BODY_MAX_LINES)"
  511. _detail "Move detailed content to references/ for on-demand loading"
  512. ci_annotate "warning" "file=$SKILL_FILE::Body exceeds $BODY_MAX_LINES lines"
  513. else
  514. _pass "Body: $body_lines lines (limit: $BODY_MAX_LINES)"
  515. fi
  516. local lines_gauge
  517. lines_gauge=$(bar_gauge_colored "$body_lines" "$BODY_MAX_LINES" 20 "$(( BODY_MAX_LINES * 70 / 100 ))" "$BODY_MAX_LINES")
  518. [ "$OUTPUT_MODE" = "terminal" ] && echo " $lines_gauge" || true
  519. # Token budget with bar gauge
  520. if [ "$token_est" -gt "$TOKEN_BUDGET" ]; then
  521. _warn "~$token_est tokens (spec recommends <$TOKEN_BUDGET on activation)"
  522. _detail "Progressive disclosure: SKILL.md body loads fully on activation"
  523. _detail "Keep instructions concise, move reference material to references/"
  524. ci_annotate "warning" "file=$SKILL_FILE::Estimated $token_est tokens exceeds $TOKEN_BUDGET budget"
  525. else
  526. _pass "~$token_est tokens (budget: $TOKEN_BUDGET)"
  527. fi
  528. local token_gauge
  529. token_gauge=$(bar_gauge_colored "$token_est" "$TOKEN_DANGER_ZONE" 20 "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE")
  530. [ "$OUTPUT_MODE" = "terminal" ] && echo " $token_gauge" || true
  531. # Structure analysis
  532. local heading_count
  533. heading_count=$(tail -n +"$((body_start + 1))" "$SKILL_FILE" | grep -c '^#' 2>/dev/null || true)
  534. heading_count=${heading_count:-0}
  535. _S_HEADINGS=$heading_count
  536. if [ "$heading_count" -eq 0 ]; then
  537. _warn "No headings in body โ€” add structure for readability"
  538. else
  539. _pass "$heading_count heading(s) providing structure"
  540. fi
  541. # Code examples
  542. local fence_pattern='```'
  543. local code_fence_count
  544. code_fence_count=$(grep -cF "$fence_pattern" "$SKILL_FILE" 2>/dev/null || true)
  545. code_fence_count=${code_fence_count:-0}
  546. local code_pairs=$(( code_fence_count / 2 ))
  547. _S_CODE_BLOCKS=$code_pairs
  548. local inline_code_count
  549. inline_code_count=$(grep -c '`[^`]' "$SKILL_FILE" 2>/dev/null || true)
  550. inline_code_count=${inline_code_count:-0}
  551. if [ "$code_pairs" -gt 0 ]; then
  552. _pass "$code_pairs fenced code example(s) in body"
  553. elif [ "$inline_code_count" -gt 0 ]; then
  554. _pass "$inline_code_count line(s) with inline code references"
  555. else
  556. _info "No code examples in body โ€” consider adding for clarity"
  557. fi
  558. # Reference links (progressive disclosure pattern)
  559. local ref_link_count
  560. ref_link_count=$(tail -n +"$((body_start + 1))" "$SKILL_FILE" | grep -cE '\]\(references/' 2>/dev/null || true)
  561. ref_link_count=${ref_link_count:-0}
  562. _S_REF_LINKS=$ref_link_count
  563. if [ "$ref_link_count" -gt 0 ]; then
  564. _pass "$ref_link_count reference link(s) โ€” using progressive disclosure"
  565. elif [ -d "references" ]; then
  566. local ref_file_count
  567. ref_file_count=$(find references -name '*.md' -type f | wc -l | tr -d ' ')
  568. if [ "$ref_file_count" -gt 0 ]; then
  569. _warn "references/ has $ref_file_count files but body has no links to them"
  570. _detail "Link references from SKILL.md body so agents can load them on demand"
  571. fi
  572. fi
  573. }
  574. # โ”€โ”€ [4] Internal Links โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  575. check_links() {
  576. [ ! -f "$SKILL_FILE" ] && return
  577. section "Internal Links"
  578. local link_data
  579. link_data=$(grep -nF '](' "$SKILL_FILE" || true)
  580. if [ -z "$link_data" ]; then
  581. _info "No internal links in SKILL.md"
  582. return
  583. fi
  584. local checked=0 broken=0
  585. while IFS= read -r match; do
  586. local line_num line_content
  587. line_num=$(echo "$match" | cut -d: -f1)
  588. line_content=$(echo "$match" | cut -d: -f2-)
  589. local targets
  590. targets=$(echo "$line_content" | tr ']' '\n' | grep '^(' | sed 's/^(\([^)]*\)).*/\1/' | sed '/^$/d')
  591. [ -z "$targets" ] && continue
  592. while IFS= read -r link_path; do
  593. case "$link_path" in http*|https*|"#"*|"") continue ;; esac
  594. local file_path
  595. file_path=$(echo "$link_path" | cut -d'#' -f1)
  596. [ -z "$file_path" ] && continue
  597. checked=$((checked + 1))
  598. if [ -f "$file_path" ]; then
  599. _pass "Line $line_num: $link_path โ†’ $(file_lines "$file_path") lines"
  600. else
  601. _error "Line $line_num: $link_path โ†’ FILE NOT FOUND"
  602. ci_annotate "error" "file=$SKILL_FILE,line=$line_num::Broken link: $link_path"
  603. broken=$((broken + 1))
  604. fi
  605. done <<< "$targets"
  606. done <<< "$link_data"
  607. echo ""
  608. _detail "Checked $checked link(s), $broken broken"
  609. }
  610. # โ”€โ”€ [5] Reference Files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  611. check_references() {
  612. [ ! -d "references" ] && return
  613. section "Reference Files"
  614. local ref_links=""
  615. if [ -f "$SKILL_FILE" ]; then
  616. ref_links=$(grep -oE '\]\(references/[^)]+\)' "$SKILL_FILE" | sed 's/\](\(.*\))/\1/' | cut -d'#' -f1 | sort -u || true)
  617. fi
  618. local total=0 linked=0 orphaned=0 empty=0
  619. echo ""
  620. printf " $(_dim " %-35s %-8s %-8s %s")\n" "FILE" "LINES" "STATUS" ""
  621. echo " $(_dim " $(printf '%.0sโ”€' $(seq 1 56))")"
  622. while IFS= read -r file; do
  623. total=$((total + 1))
  624. local lines
  625. lines=$(file_lines "$file")
  626. if [ "$lines" -eq 0 ]; then
  627. printf " $(_yellow " %-35s %-8s %-8s")\n" "$(basename "$file")" "0" "EMPTY"
  628. _warn "$file is empty (0 lines)"
  629. ci_annotate "warning" "file=$file::Empty reference file"
  630. empty=$((empty + 1))
  631. continue
  632. fi
  633. if echo "$ref_links" | grep -qF "$file"; then
  634. linked=$((linked + 1))
  635. printf " $(_green " %-35s %-8s %-8s")\n" "$(basename "$file")" "$lines" "LINKED"
  636. else
  637. orphaned=$((orphaned + 1))
  638. printf " $(_yellow " %-35s %-8s %-8s")\n" "$(basename "$file")" "$lines" "ORPHAN"
  639. _warn "$file โ€” $(_yellow "orphaned")"
  640. _detail "Not linked from SKILL.md โ€” agents won't discover this file"
  641. ci_annotate "warning" "file=$file::Not linked from SKILL.md (orphaned)"
  642. fi
  643. done < <(find references -name '*.md' -type f | sort)
  644. _S_REFS_TOTAL=$total
  645. _S_REFS_LINKED=$linked
  646. # Missing references (linked in SKILL.md but don't exist)
  647. local missing=0
  648. if [ -n "$ref_links" ]; then
  649. for ref in $ref_links; do
  650. if [ ! -f "$ref" ]; then
  651. _error "$ref โ†’ linked in SKILL.md but file is missing"
  652. ci_annotate "error" "file=$ref::Referenced in SKILL.md but does not exist"
  653. missing=$((missing + 1))
  654. fi
  655. done
  656. fi
  657. echo ""
  658. echo " $(_dim " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”")"
  659. printf " $(_dim " โ”‚") Total: %-4s Linked: %-4s Orphaned: %-4s$(_dim "โ”‚")\n" "$total" "$linked" "$orphaned"
  660. printf " $(_dim " โ”‚") Empty: %-4s Missing: %-4s $(_dim "โ”‚")\n" "$empty" "$missing"
  661. echo " $(_dim " โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜")"
  662. }
  663. # โ”€โ”€ [6] Markdown Syntax โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  664. check_markdown() {
  665. section "Markdown Syntax"
  666. local pattern file_count=0 issues=0
  667. pattern='```'
  668. while IFS= read -r file; do
  669. file_count=$((file_count + 1))
  670. local count
  671. count=$(grep -cF "$pattern" "$file" 2>/dev/null || true)
  672. count=${count:-0}
  673. if [ "$count" -gt 0 ] && [ $((count % 2)) -ne 0 ]; then
  674. _warn "$file โ€” unclosed code block ($count fences)"
  675. grep -nF "$pattern" "$file" 2>/dev/null | while IFS= read -r m; do
  676. _detail "Line $(echo "$m" | cut -d: -f1): $(echo "$m" | cut -d: -f2-)"
  677. done
  678. ci_annotate "warning" "file=$file::Unclosed code block ($count fences)"
  679. issues=$((issues + 1))
  680. fi
  681. done < <(find . -name '*.md' -not -path './.git/*' | sort)
  682. if [ "$issues" -eq 0 ]; then
  683. _pass "All $file_count markdown files have balanced code fences"
  684. else
  685. _S_FENCES_OK=false
  686. fi
  687. }
  688. # โ”€โ”€ [7] Reference Nesting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  689. check_reference_depth() {
  690. [ ! -d "references" ] && return
  691. section "Reference Nesting"
  692. local deep_refs=0
  693. while IFS= read -r file; do
  694. local nested
  695. nested=$(grep -cE '\]\(references/' "$file" 2>/dev/null || true)
  696. if [ "$nested" -gt 0 ]; then
  697. _warn "$file โ†’ $nested cross-reference(s) to other reference files"
  698. _detail "Spec: keep file references one level deep from SKILL.md"
  699. ci_annotate "warning" "file=$file::Cross-references between reference files"
  700. deep_refs=$((deep_refs + 1))
  701. fi
  702. done < <(find references -name '*.md' -type f | sort)
  703. if [ "$deep_refs" -eq 0 ]; then
  704. _pass "No nested reference chains โ€” clean one-level structure"
  705. else
  706. _detail "$deep_refs file(s) with cross-references"
  707. fi
  708. }
  709. # โ”€โ”€ [8] Scripts Validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  710. check_scripts() {
  711. [ ! -d "scripts" ] && return
  712. section "Scripts"
  713. local total=0 executable=0 not_executable=0 documented=0
  714. while IFS= read -r script; do
  715. total=$((total + 1))
  716. local basename_script
  717. basename_script=$(basename "$script")
  718. if [ -x "$script" ]; then
  719. executable=$((executable + 1))
  720. _pass "$script (executable, $(file_lines "$script") lines)"
  721. else
  722. not_executable=$((not_executable + 1))
  723. _warn "$script is not executable"
  724. _detail "Run: chmod +x $script"
  725. ci_annotate "warning" "file=$script::Script is not executable"
  726. fi
  727. if [ -f "$SKILL_FILE" ] && grep -qF "$basename_script" "$SKILL_FILE"; then
  728. documented=$((documented + 1))
  729. fi
  730. done < <(find scripts -type f | sort)
  731. if [ "$total" -gt 0 ] && [ "$documented" -eq 0 ] && [ -f "$SKILL_FILE" ]; then
  732. _info "No scripts referenced in SKILL.md body"
  733. _detail "Document available scripts so agents know what tools they can run"
  734. elif [ "$documented" -gt 0 ]; then
  735. _pass "$documented of $total script(s) documented in SKILL.md"
  736. fi
  737. }
  738. # โ”€โ”€ [9] Package Integrity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  739. check_repo_hygiene() {
  740. section "Repository Hygiene"
  741. local sensitive
  742. sensitive=$(find . -maxdepth 3 \
  743. \( -name '.env' -o -name '*.key' -o -name '*.pem' -o -name 'credentials*' \) \
  744. -not -path './.git/*' 2>/dev/null || true)
  745. if [ -n "$sensitive" ]; then
  746. _warn "Possible sensitive files detected"
  747. echo "$sensitive" | while IFS= read -r f; do
  748. _detail " $f"
  749. done
  750. else
  751. _pass "No sensitive files (.env, .key, .pem, credentials)"
  752. fi
  753. local artifacts
  754. artifacts=$(find . -maxdepth 3 \
  755. \( -name 'node_modules' -o -name '__pycache__' -o -name '.DS_Store' \) \
  756. -not -path './.git/*' 2>/dev/null || true)
  757. if [ -n "$artifacts" ]; then
  758. _warn "Development artifacts found โ€” add to .gitignore"
  759. echo "$artifacts" | while IFS= read -r f; do
  760. _detail " $f"
  761. done
  762. else
  763. _pass "No development artifacts (node_modules, __pycache__, .DS_Store)"
  764. fi
  765. }
  766. # โ”€โ”€ [10] Token Budget Analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  767. check_token_budget() {
  768. section "Token Budget Analysis"
  769. if [ "$OUTPUT_MODE" = "terminal" ]; then echo ""; fi
  770. _detail "Token estimation: chars/4 (tiktoken cl100k_base approximation)"
  771. _detail "Zone thresholds based on agentskills.io and context engineering best practices"
  772. if [ "$OUTPUT_MODE" = "terminal" ]; then echo ""; fi
  773. local total_tokens=0
  774. local total_chars=0
  775. local total_words=0
  776. local total_lines=0
  777. local file_count=0
  778. # โ”€โ”€ SKILL.md analysis โ”€โ”€
  779. if [ -f "$SKILL_FILE" ]; then
  780. local skill_data skill_tokens skill_chars skill_words skill_lines
  781. skill_data=$(estimate_tokens_detailed "$SKILL_FILE")
  782. skill_tokens=$(echo "$skill_data" | cut -d'|' -f1)
  783. skill_chars=$(echo "$skill_data" | cut -d'|' -f2)
  784. skill_words=$(echo "$skill_data" | cut -d'|' -f3)
  785. skill_lines=$(echo "$skill_data" | cut -d'|' -f4)
  786. local zone_indicator
  787. zone_indicator=$(token_zone_indicator "$skill_tokens" "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE")
  788. if [ "$OUTPUT_MODE" = "terminal" ]; then
  789. echo " $(_bold "SKILL.md") $zone_indicator"
  790. echo " โ”œโ”€ $(format_tokens_colored "$skill_tokens" "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE" "tokens")"
  791. local skill_gauge
  792. skill_gauge=$(bar_gauge_colored "$skill_tokens" "$TOKEN_DANGER_ZONE" 20 "$TOKEN_SAFE_ZONE" "$TOKEN_WARN_ZONE")
  793. echo " โ”œโ”€ $skill_gauge"
  794. echo " โ”œโ”€ $skill_chars chars | $skill_words words | $skill_lines lines"
  795. echo " โ””โ”€ Budget: $TOKEN_WARN_ZONE tokens recommended"
  796. fi
  797. total_tokens=$((total_tokens + skill_tokens))
  798. total_chars=$((total_chars + skill_chars))
  799. total_words=$((total_words + skill_words))
  800. total_lines=$((total_lines + skill_lines))
  801. file_count=$((file_count + 1))
  802. if [ "$skill_tokens" -gt "$TOKEN_DANGER_ZONE" ]; then
  803. _warn "SKILL.md exceeds danger zone (~$skill_tokens tokens > $TOKEN_DANGER_ZONE)"
  804. _detail "Consider moving content to references/ for on-demand loading"
  805. fi
  806. fi
  807. # โ”€โ”€ Reference files analysis โ”€โ”€
  808. if [ -d "references" ]; then
  809. if [ "$OUTPUT_MODE" = "terminal" ]; then
  810. echo ""
  811. echo " $(_bold "Reference Files:")"
  812. echo ""
  813. printf " $(_dim "%-35s %-12s %-6s %-6s")\n" "FILE" "ZONE" "TOKENS" "LINES"
  814. echo " $(_dim "$(printf '%.0sโ”€' $(seq 1 62))")"
  815. fi
  816. local ref_total=0
  817. local ref_files=0
  818. local largest_ref=""
  819. local largest_ref_tokens=0
  820. while IFS= read -r file; do
  821. local ref_data ref_tokens ref_chars ref_words ref_lines
  822. ref_data=$(estimate_tokens_detailed "$file")
  823. ref_tokens=$(echo "$ref_data" | cut -d'|' -f1)
  824. ref_chars=$(echo "$ref_data" | cut -d'|' -f2)
  825. ref_words=$(echo "$ref_data" | cut -d'|' -f3)
  826. ref_lines=$(echo "$ref_data" | cut -d'|' -f4)
  827. local zone_indicator
  828. zone_indicator=$(token_zone_indicator "$ref_tokens" "$REF_TOKEN_SAFE" "$REF_TOKEN_WARN")
  829. local basename_file
  830. basename_file=$(basename "$file")
  831. [ "$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
  832. ref_total=$((ref_total + ref_tokens))
  833. ref_files=$((ref_files + 1))
  834. total_tokens=$((total_tokens + ref_tokens))
  835. total_chars=$((total_chars + ref_chars))
  836. total_words=$((total_words + ref_words))
  837. total_lines=$((total_lines + ref_lines))
  838. file_count=$((file_count + 1))
  839. if [ "$ref_tokens" -gt "$largest_ref_tokens" ]; then
  840. largest_ref_tokens=$ref_tokens
  841. largest_ref="$basename_file"
  842. fi
  843. if [ "$ref_tokens" -gt "$REF_TOKEN_DANGER" ]; then
  844. _warn "$basename_file is too large (~$ref_tokens tokens > $REF_TOKEN_DANGER)"
  845. _detail "Split into base + advanced files (e.g., topic.md + topic-advanced.md)"
  846. elif [ "$ref_tokens" -gt "$REF_TOKEN_WARN" ]; then
  847. _warn "$basename_file is getting large (~$ref_tokens tokens > $REF_TOKEN_WARN)"
  848. _detail "Tighten prose: tables over paragraphs, cross-link shared patterns, trim tutorial content"
  849. fi
  850. done < <(find references -name '*.md' -type f | sort)
  851. if [ "$OUTPUT_MODE" = "terminal" ]; then
  852. echo ""
  853. echo " $(_dim "Reference subtotal: ~$ref_total tokens across $ref_files files")"
  854. if [ -n "$largest_ref" ]; then
  855. echo " $(_dim "Largest reference: $largest_ref (~$largest_ref_tokens tokens)")"
  856. fi
  857. fi
  858. fi
  859. # โ”€โ”€ README analysis โ”€โ”€
  860. if [ -f "README.md" ]; then
  861. local readme_data readme_tokens readme_lines
  862. readme_data=$(estimate_tokens_detailed "README.md")
  863. readme_tokens=$(echo "$readme_data" | cut -d'|' -f1)
  864. readme_lines=$(echo "$readme_data" | cut -d'|' -f4)
  865. if [ "$OUTPUT_MODE" = "terminal" ]; then
  866. echo ""
  867. echo " $(_bold "README.md") $(_dim "(not loaded by agents, for humans)")"
  868. echo " โ””โ”€ ~$readme_tokens tokens | $readme_lines lines"
  869. fi
  870. fi
  871. # โ”€โ”€ Total package summary โ”€โ”€
  872. if [ "$OUTPUT_MODE" = "terminal" ]; then
  873. echo ""
  874. echo " $(_bold "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”")"
  875. local total_zone_indicator
  876. total_zone_indicator=$(token_zone_indicator "$total_tokens" "$TOTAL_TOKEN_SAFE" "$TOTAL_TOKEN_WARN")
  877. echo " $(_bold "TOTAL SKILL PACKAGE") $total_zone_indicator"
  878. echo ""
  879. echo " $(format_tokens_colored "$total_tokens" "$TOTAL_TOKEN_SAFE" "$TOTAL_TOKEN_WARN" "tokens") (estimated)"
  880. local total_gauge
  881. total_gauge=$(bar_gauge_colored "$total_tokens" "$TOTAL_TOKEN_DANGER" 25 "$TOTAL_TOKEN_SAFE" "$TOTAL_TOKEN_WARN")
  882. echo " $total_gauge"
  883. echo " $total_chars chars | $total_words words | $total_lines lines | $file_count files"
  884. echo ""
  885. fi
  886. local uses_progressive=false
  887. if [ -d "references" ] && [ -f "$SKILL_FILE" ]; then
  888. local ref_link_count
  889. ref_link_count=$(grep -cE '\]\(references/' "$SKILL_FILE" 2>/dev/null || true)
  890. ref_link_count=${ref_link_count:-0}
  891. if [ "$ref_link_count" -gt 0 ]; then
  892. uses_progressive=true
  893. fi
  894. fi
  895. local ctx_200k_pct ctx_128k_pct skill_only_200k_pct
  896. ctx_200k_pct=$(( (total_tokens * 100) / 200000 ))
  897. ctx_128k_pct=$(( (total_tokens * 100) / 128000 ))
  898. if [ -f "$SKILL_FILE" ]; then
  899. skill_only_200k_pct=$(( (_S_BODY_TOKENS * 100) / 200000 ))
  900. else
  901. skill_only_200k_pct=0
  902. fi
  903. if [ "$OUTPUT_MODE" = "terminal" ]; then
  904. echo " $(_bold "Context Window Impact:")"
  905. echo " $(_dim "Agent tools (Cursor, Windsurf, Copilot) typically use ~200K context")"
  906. echo " $(_dim "regardless of the underlying model's max window.")"
  907. echo ""
  908. echo " โ”œโ”€ Agent context (200K): ~${ctx_200k_pct}% if all files loaded"
  909. echo " โ”œโ”€ Agent context (128K): ~${ctx_128k_pct}% if all files loaded"
  910. if [ "$uses_progressive" = true ]; then
  911. echo " โ”œโ”€ $(_green "SKILL.md only: ~${skill_only_200k_pct}% of 200K") (initial activation cost)"
  912. echo " โ””โ”€ $(_dim "Progressive disclosure โ€” references load on demand, not upfront")"
  913. else
  914. echo " โ””โ”€ $(_yellow "Monolithic โ€” entire skill loads at once (no references/ linked)")"
  915. fi
  916. echo ""
  917. fi
  918. if [ "$uses_progressive" = true ]; then
  919. if [ "$total_tokens" -le "$TOTAL_TOKEN_SAFE" ]; then
  920. _pass "Package is well-optimized for token efficiency"
  921. elif [ "$total_tokens" -le "$TOTAL_TOKEN_WARN" ]; then
  922. _pass "Package size is fine โ€” progressive disclosure loads files on demand"
  923. else
  924. _info "Large package (~$total_tokens tokens) โ€” ensure no dead/orphaned references"
  925. _detail "Total size is informational with progressive disclosure; individual file sizes are the real quality gate"
  926. fi
  927. else
  928. if [ "$total_tokens" -le "$TOKEN_BUDGET" ]; then
  929. _pass "Monolithic skill is within token budget"
  930. elif [ "$total_tokens" -le "$TOKEN_DANGER_ZONE" ]; then
  931. _warn "Monolithic skill (~$total_tokens tokens) exceeds $TOKEN_BUDGET budget"
  932. _detail "Move detailed content to references/ for on-demand loading"
  933. else
  934. _warn "Monolithic skill (~$total_tokens tokens) is very large"
  935. _detail "Split into SKILL.md + references/ for progressive disclosure"
  936. fi
  937. fi
  938. }
  939. # โ”€โ”€ [11] Content Quality Metrics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  940. check_content_quality() {
  941. section "Content Quality Metrics"
  942. local total_code_blocks=0
  943. local total_headings=0
  944. if [ -f "$SKILL_FILE" ]; then
  945. local code_blocks headings internal_links external_links
  946. code_blocks=$(safe_count grep -c '```' "$SKILL_FILE")
  947. code_blocks=$((code_blocks / 2))
  948. headings=$(safe_count grep -c '^#' "$SKILL_FILE")
  949. internal_links=$(safe_count grep -c '\](references/' "$SKILL_FILE")
  950. external_links=$(safe_count grep -cE '\]\(https?://' "$SKILL_FILE")
  951. echo ""
  952. echo " $(_bold "SKILL.md Structure:")"
  953. echo " โ”œโ”€ $headings heading(s) providing navigation"
  954. echo " โ”œโ”€ $code_blocks code example(s)"
  955. echo " โ”œโ”€ $internal_links internal link(s) to references"
  956. echo " โ””โ”€ $external_links external link(s)"
  957. total_code_blocks=$code_blocks
  958. total_headings=$headings
  959. if [ "$headings" -lt 3 ]; then
  960. _info "Consider adding more headings for better navigation"
  961. fi
  962. if [ "$code_blocks" -lt 2 ]; then
  963. _info "Consider adding code examples for clarity"
  964. fi
  965. fi
  966. if [ -d "references" ]; then
  967. echo ""
  968. echo " $(_bold "Reference Files Quality:")"
  969. local ref_with_code=0
  970. local ref_without_code=0
  971. local total_ref_headings=0
  972. while IFS= read -r file; do
  973. local file_code_blocks file_headings
  974. file_code_blocks=$(safe_count grep -c '```' "$file")
  975. file_code_blocks=$((file_code_blocks / 2))
  976. file_headings=$(safe_count grep -c '^#' "$file")
  977. total_code_blocks=$((total_code_blocks + file_code_blocks))
  978. total_ref_headings=$((total_ref_headings + file_headings))
  979. if [ "$file_code_blocks" -gt 0 ]; then
  980. ref_with_code=$((ref_with_code + 1))
  981. else
  982. ref_without_code=$((ref_without_code + 1))
  983. fi
  984. done < <(find references -name '*.md' -type f)
  985. local ref_count
  986. ref_count=$(find references -name '*.md' -type f | wc -l | tr -d ' ')
  987. echo " โ”œโ”€ $ref_with_code of $ref_count files contain code examples"
  988. echo " โ”œโ”€ $total_ref_headings total headings across references"
  989. echo " โ””โ”€ $total_code_blocks total code blocks in package"
  990. if [ "$ref_without_code" -gt 0 ]; then
  991. _info "$ref_without_code reference file(s) have no code examples"
  992. _detail "Code examples help agents understand expected patterns"
  993. fi
  994. fi
  995. # Keyword density analysis
  996. if [ -f "$SKILL_FILE" ]; then
  997. echo ""
  998. echo " $(_bold "Trigger Keyword Analysis:")"
  999. local compose_mentions kotlin_mentions android_mentions kmp_mentions
  1000. compose_mentions=$(grep -ioE '(compose|composable|@Composable|jetpack)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
  1001. kotlin_mentions=$(grep -ioE '(kotlin|coroutine|flow|stateflow|channel)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
  1002. android_mentions=$(grep -ioE '(android|viewmodel|hilt|koin|room|datastore)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
  1003. kmp_mentions=$(grep -ioE '(multiplatform|kmp|cmp|commonmain|ios|desktop)' "$SKILL_FILE" 2>/dev/null | wc -l | tr -d ' ' || true)
  1004. compose_mentions=${compose_mentions:-0}
  1005. kotlin_mentions=${kotlin_mentions:-0}
  1006. android_mentions=${android_mentions:-0}
  1007. kmp_mentions=${kmp_mentions:-0}
  1008. echo " โ”œโ”€ Compose/UI: $compose_mentions mentions"
  1009. echo " โ”œโ”€ Kotlin/Flow: $kotlin_mentions mentions"
  1010. echo " โ”œโ”€ Android/DI: $android_mentions mentions"
  1011. echo " โ””โ”€ Multiplatform: $kmp_mentions mentions"
  1012. local total_keywords=$((compose_mentions + kotlin_mentions + android_mentions + kmp_mentions))
  1013. if [ "$total_keywords" -lt 10 ]; then
  1014. _info "Low keyword density may reduce skill trigger accuracy"
  1015. _detail "Ensure description and body mention key terms agents should recognize"
  1016. else
  1017. _pass "Good keyword coverage for skill triggering ($total_keywords mentions)"
  1018. fi
  1019. fi
  1020. }
  1021. # โ”€โ”€ [12] Agent Metadata โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  1022. check_agents_metadata() {
  1023. section "Agent Metadata (Codex)"
  1024. if [ ! -f "agents/openai.yaml" ]; then
  1025. _info "No agents/openai.yaml โ€” optional, configures Codex app UI and policy"
  1026. return
  1027. fi
  1028. _pass "agents/openai.yaml present"
  1029. if grep -q '^interface:' agents/openai.yaml; then
  1030. if grep -q 'display_name:' agents/openai.yaml; then
  1031. local display_name
  1032. display_name=$(grep 'display_name:' agents/openai.yaml | sed 's/.*display_name:[[:space:]]*//' | tr -d '"')
  1033. _pass "display_name: '$display_name'"
  1034. else
  1035. _info "No display_name โ€” Codex uses skill name"
  1036. fi
  1037. if grep -q 'short_description:' agents/openai.yaml; then
  1038. _pass "short_description set"
  1039. else
  1040. _info "No short_description โ€” Codex uses SKILL.md description"
  1041. fi
  1042. if grep -q 'default_prompt:' agents/openai.yaml; then
  1043. _pass "default_prompt set"
  1044. fi
  1045. if grep -q 'brand_color:' agents/openai.yaml; then
  1046. _pass "brand_color set"
  1047. fi
  1048. local icon_fields
  1049. icon_fields=$(grep -oE '(icon_small|icon_large):[[:space:]]*"[^"]+"' agents/openai.yaml 2>/dev/null || true)
  1050. if [ -n "$icon_fields" ]; then
  1051. echo "$icon_fields" | while IFS= read -r line; do
  1052. local field icon_path
  1053. field=$(echo "$line" | cut -d: -f1 | tr -d ' ')
  1054. icon_path=$(echo "$line" | sed 's/.*"\(.*\)"/\1/')
  1055. if [ -f "$icon_path" ]; then
  1056. _pass "$field: $icon_path"
  1057. else
  1058. _warn "$field: '$icon_path' โ†’ file not found"
  1059. ci_annotate "warning" "file=agents/openai.yaml::$field file missing: $icon_path"
  1060. fi
  1061. done
  1062. fi
  1063. fi
  1064. if grep -q '^policy:' agents/openai.yaml; then
  1065. if grep -q 'allow_implicit_invocation:' agents/openai.yaml; then
  1066. local implicit
  1067. implicit=$(grep 'allow_implicit_invocation:' agents/openai.yaml | sed 's/.*allow_implicit_invocation:[[:space:]]*//')
  1068. _pass "allow_implicit_invocation: $implicit"
  1069. fi
  1070. fi
  1071. if grep -q '^dependencies:' agents/openai.yaml; then
  1072. _pass "dependencies declared"
  1073. fi
  1074. }
  1075. # โ”€โ”€ [13] Security Scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  1076. check_security() {
  1077. section "Security Scan"
  1078. local secret_count=0
  1079. local path_count=0
  1080. local cmd_count=0
  1081. # โ”€โ”€ Secrets detection (11 patterns from skill-tools / skill-validator) โ”€โ”€
  1082. local -a secret_patterns=(
  1083. 'sk-[a-zA-Z0-9]{20,}'
  1084. 'sk_live_[a-zA-Z0-9]+'
  1085. 'sk_test_[a-zA-Z0-9]+'
  1086. 'ghp_[a-zA-Z0-9]{36}'
  1087. 'gho_[a-zA-Z0-9]{36}'
  1088. 'ghu_[a-zA-Z0-9]{36}'
  1089. 'ghs_[a-zA-Z0-9]{36}'
  1090. 'ghr_[a-zA-Z0-9]{36}'
  1091. 'xoxb-[a-zA-Z0-9-]+'
  1092. 'xoxp-[a-zA-Z0-9-]+'
  1093. 'AKIA[0-9A-Z]{16}'
  1094. )
  1095. while IFS= read -r file; do
  1096. for pattern in "${secret_patterns[@]}"; do
  1097. local matches
  1098. matches=$(grep -nE "$pattern" "$file" 2>/dev/null | head -1 || true)
  1099. if [ -n "$matches" ]; then
  1100. secret_count=$((secret_count + 1))
  1101. local line_num
  1102. line_num=$(echo "$matches" | cut -d: -f1)
  1103. _error "$file:$line_num โ€” potential secret/API key detected"
  1104. _detail "Pattern: $pattern"
  1105. ci_annotate "error" "file=$file,line=$line_num::Potential secret detected"
  1106. fi
  1107. done
  1108. # PEM private key headers
  1109. local pem_match
  1110. pem_match=$(grep -n 'BEGIN.*PRIVATE KEY' "$file" 2>/dev/null | head -1 || true)
  1111. if [ -n "$pem_match" ]; then
  1112. secret_count=$((secret_count + 1))
  1113. local line_num
  1114. line_num=$(echo "$pem_match" | cut -d: -f1)
  1115. _error "$file:$line_num โ€” private key header detected"
  1116. ci_annotate "error" "file=$file,line=$line_num::Private key detected"
  1117. fi
  1118. # JWT tokens
  1119. local jwt_match
  1120. jwt_match=$(grep -nE 'eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.' "$file" 2>/dev/null | head -1 || true)
  1121. if [ -n "$jwt_match" ]; then
  1122. secret_count=$((secret_count + 1))
  1123. local line_num
  1124. line_num=$(echo "$jwt_match" | cut -d: -f1)
  1125. _error "$file:$line_num โ€” JWT token detected"
  1126. ci_annotate "error" "file=$file,line=$line_num::JWT token detected"
  1127. fi
  1128. done < <(find . -name '*.md' -not -path './.git/*' | sort)
  1129. _S_SECRETS=$secret_count
  1130. if [ "$secret_count" -eq 0 ]; then
  1131. _pass "No API keys or secrets detected (13 patterns checked)"
  1132. fi
  1133. # โ”€โ”€ Hardcoded absolute paths โ”€โ”€
  1134. echo ""
  1135. _detail "Checking for hardcoded paths..."
  1136. while IFS= read -r file; do
  1137. local path_matches
  1138. path_matches=$(grep -nE '(/Users/[a-zA-Z]|/home/[a-zA-Z]|C:\\Users\\)' "$file" 2>/dev/null || true)
  1139. if [ -n "$path_matches" ]; then
  1140. while IFS= read -r match; do
  1141. path_count=$((path_count + 1))
  1142. local line_num
  1143. line_num=$(echo "$match" | cut -d: -f1)
  1144. _warn "$file:$line_num โ€” hardcoded absolute path"
  1145. _detail "$(echo "$match" | cut -d: -f2- | sed 's/^[[:space:]]*//' | head -c 80)"
  1146. done <<< "$path_matches"
  1147. fi
  1148. done < <(find . -name '*.md' -not -path './.git/*' | sort)
  1149. _S_HARDCODED=$path_count
  1150. if [ "$path_count" -eq 0 ]; then
  1151. _pass "No hardcoded absolute paths"
  1152. fi
  1153. # โ”€โ”€ Dangerous commands โ”€โ”€
  1154. echo ""
  1155. _detail "Checking for dangerous commands..."
  1156. local -a dangerous_patterns=(
  1157. 'rm -rf /'
  1158. 'sudo rm '
  1159. 'DROP TABLE'
  1160. 'DROP DATABASE'
  1161. 'chmod 777'
  1162. )
  1163. local -a dangerous_regex=(
  1164. 'curl .*\| *sh'
  1165. 'wget .*\| *sh'
  1166. )
  1167. while IFS= read -r file; do
  1168. for pattern in "${dangerous_patterns[@]}"; do
  1169. local matches
  1170. matches=$(grep -nF "$pattern" "$file" 2>/dev/null | head -1 || true)
  1171. if [ -n "$matches" ]; then
  1172. cmd_count=$((cmd_count + 1))
  1173. local line_num
  1174. line_num=$(echo "$matches" | cut -d: -f1)
  1175. _warn "$file:$line_num โ€” potentially dangerous command"
  1176. _detail "Matched: $pattern"
  1177. ci_annotate "warning" "file=$file,line=$line_num::Dangerous command pattern"
  1178. fi
  1179. done
  1180. for pattern in "${dangerous_regex[@]}"; do
  1181. local matches
  1182. matches=$(grep -nE "$pattern" "$file" 2>/dev/null | head -1 || true)
  1183. if [ -n "$matches" ]; then
  1184. cmd_count=$((cmd_count + 1))
  1185. local line_num
  1186. line_num=$(echo "$matches" | cut -d: -f1)
  1187. _warn "$file:$line_num โ€” potentially dangerous command"
  1188. _detail "Matched: $pattern"
  1189. ci_annotate "warning" "file=$file,line=$line_num::Dangerous command pattern"
  1190. fi
  1191. done
  1192. done < <(find . -name '*.md' -not -path './.git/*' | sort)
  1193. _S_DANGEROUS=$cmd_count
  1194. if [ "$cmd_count" -eq 0 ]; then
  1195. _pass "No dangerous shell commands detected"
  1196. fi
  1197. echo ""
  1198. echo " $(_dim " โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”")"
  1199. printf " $(_dim " โ”‚") Secrets: %-4s Paths: %-4s Commands: %-4s $(_dim "โ”‚")\n" "$secret_count" "$path_count" "$cmd_count"
  1200. echo " $(_dim " โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜")"
  1201. }
  1202. # โ”€โ”€ [14] Heading Hierarchy & Duplicates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  1203. check_heading_hierarchy() {
  1204. section "Heading Hierarchy & Duplicates"
  1205. local skip_issues=0
  1206. local dup_issues=0
  1207. local files_checked=0
  1208. while IFS= read -r file; do
  1209. files_checked=$((files_checked + 1))
  1210. local prev_level=0
  1211. local line_num=0
  1212. local headings_seen=""
  1213. while IFS= read -r line; do
  1214. line_num=$((line_num + 1))
  1215. case "$line" in
  1216. '#'*)
  1217. local stripped_prefix
  1218. stripped_prefix="${line%%[^#]*}"
  1219. local level=${#stripped_prefix}
  1220. local text
  1221. text=$(echo "$line" | sed 's/^#* *//')
  1222. # MD001: heading level increment check
  1223. if [ "$prev_level" -gt 0 ] && [ "$level" -gt "$((prev_level + 1))" ]; then
  1224. skip_issues=$((skip_issues + 1))
  1225. _warn "$file:$line_num โ€” heading skip H$prev_level โ†’ H$level"
  1226. _detail "$text"
  1227. ci_annotate "warning" "file=$file,line=$line_num::Heading level skip (H$prev_level to H$level)"
  1228. fi
  1229. prev_level=$level
  1230. # MD024: duplicate heading check (same level, same text)
  1231. local key="${level}:${text}"
  1232. if echo "$headings_seen" | grep -qF "<<${key}>>"; then
  1233. dup_issues=$((dup_issues + 1))
  1234. _warn "$file:$line_num โ€” duplicate heading: '$text' (H$level)"
  1235. ci_annotate "warning" "file=$file,line=$line_num::Duplicate heading '$text'"
  1236. fi
  1237. headings_seen="${headings_seen}<<${key}>>"
  1238. ;;
  1239. esac
  1240. done < "$file"
  1241. done < <(find . -name '*.md' -not -path './.git/*' | sort)
  1242. if [ "$skip_issues" -eq 0 ]; then
  1243. _pass "No heading level skips across $files_checked files (MD001)"
  1244. fi
  1245. if [ "$dup_issues" -eq 0 ]; then
  1246. _pass "No duplicate sibling headings across $files_checked files (MD024)"
  1247. fi
  1248. echo ""
  1249. _detail "Checked $files_checked files | Level skips: $skip_issues | Duplicates: $dup_issues"
  1250. }
  1251. # โ”€โ”€ [15] Quality Score โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  1252. compute_quality_score() {
  1253. section "Quality Score"
  1254. local desc_score=0
  1255. local clarity_score=0
  1256. local spec_score=0
  1257. local progressive_score=0
  1258. local security_score=10
  1259. _check_mark() {
  1260. if [ "$1" = "1" ]; then printf "%s" "$(_green "โœ“")"; else printf "%s" "$(_red "โœ—")"; fi
  1261. }
  1262. # โ”€โ”€ Description Quality (30 pts) โ”€โ”€
  1263. local d1=0 d2=0 d3=0 d4=0 d5=0 d6=0 d7=0
  1264. [ "$_S_HAS_DESC" = true ] && { desc_score=$((desc_score + 5)); d1=1; }
  1265. [ "$_S_DESC_LEN" -ge "$DESC_MIN_USEFUL" ] && { desc_score=$((desc_score + 3)); d2=1; }
  1266. [ "$_S_DESC_WHAT" = true ] && { desc_score=$((desc_score + 5)); d3=1; }
  1267. [ "$_S_DESC_WHEN" = true ] && { desc_score=$((desc_score + 5)); d4=1; }
  1268. [ "$_S_DESC_FIRST_PERSON" = false ] && { desc_score=$((desc_score + 4)); d5=1; }
  1269. [ "$_S_DESC_GENERIC" = false ] && { desc_score=$((desc_score + 4)); d6=1; }
  1270. [ "$_S_DESC_NEGATIVES" = true ] && { desc_score=$((desc_score + 4)); d7=1; }
  1271. # โ”€โ”€ Instruction Clarity (25 pts) โ”€โ”€
  1272. local c1=0 c2=0 c3=0 c4=0 c5=0 c6=0
  1273. [ "$_S_CODE_BLOCKS" -ge 1 ] && { clarity_score=$((clarity_score + 5)); c1=1; }
  1274. [ "$_S_CODE_BLOCKS" -ge 3 ] && { clarity_score=$((clarity_score + 5)); c2=1; }
  1275. [ "$_S_HEADINGS" -ge 3 ] && { clarity_score=$((clarity_score + 5)); c3=1; }
  1276. [ "$_S_HEADINGS" -ge 5 ] && { clarity_score=$((clarity_score + 3)); c4=1; }
  1277. [ "$_S_REF_LINKS" -gt 0 ] && { clarity_score=$((clarity_score + 4)); c5=1; }
  1278. [ "$_S_REF_LINKS" -ge 5 ] && { clarity_score=$((clarity_score + 3)); c6=1; }
  1279. # โ”€โ”€ Spec Compliance (20 pts) โ”€โ”€
  1280. local s1=0 s2=0 s3=0 s4=0 s5=0 s6=0
  1281. [ "$_S_HAS_NAME" = true ] && { spec_score=$((spec_score + 4)); s1=1; }
  1282. [ "$_S_NAME_OK" = true ] && { spec_score=$((spec_score + 4)); s2=1; }
  1283. [ "$_S_HAS_DESC" = true ] && { spec_score=$((spec_score + 4)); s3=1; }
  1284. [ "$_S_BODY_TOKENS" -le "$TOKEN_BUDGET" ] && { spec_score=$((spec_score + 4)); s4=1; }
  1285. [ "$_S_BODY_LINES" -le "$BODY_MAX_LINES" ] && { spec_score=$((spec_score + 2)); s5=1; }
  1286. [ "$_S_FENCES_OK" = true ] && { spec_score=$((spec_score + 2)); s6=1; }
  1287. # โ”€โ”€ Progressive Disclosure (15 pts) โ”€โ”€
  1288. local p1=0 p2=0 p3=0 p4=0
  1289. [ "$_S_HAS_REFS_DIR" = true ] && { progressive_score=$((progressive_score + 5)); p1=1; }
  1290. [ "$_S_REFS_LINKED" -gt 0 ] && { progressive_score=$((progressive_score + 5)); p2=1; }
  1291. if [ "$_S_REFS_TOTAL" -gt 0 ] && [ "$_S_REFS_LINKED" -eq "$_S_REFS_TOTAL" ]; then
  1292. progressive_score=$((progressive_score + 3)); p3=1
  1293. fi
  1294. [ "$_S_BODY_LINES" -lt "$BODY_MAX_LINES" ] && { progressive_score=$((progressive_score + 2)); p4=1; }
  1295. # โ”€โ”€ Security (10 pts โ€” start at 10, deduct for issues) โ”€โ”€
  1296. local x1=1 x2=1 x3=1
  1297. if [ "$_S_SECRETS" -gt 0 ]; then security_score=$((security_score - 5)); x1=0; fi
  1298. if [ "$_S_HARDCODED" -gt 0 ]; then security_score=$((security_score - 3)); x2=0; fi
  1299. if [ "$_S_DANGEROUS" -gt 0 ]; then security_score=$((security_score - 2)); x3=0; fi
  1300. [ "$security_score" -lt 0 ] && security_score=0
  1301. local total_score=$((desc_score + clarity_score + spec_score + progressive_score + security_score))
  1302. QUALITY_SCORE=$total_score
  1303. QUALITY_GRADE=$(letter_grade "$total_score")
  1304. QUALITY_DESC=$desc_score
  1305. QUALITY_CLARITY=$clarity_score
  1306. QUALITY_SPEC=$spec_score
  1307. QUALITY_PROGRESSIVE=$progressive_score
  1308. QUALITY_SECURITY=$security_score
  1309. # โ”€โ”€ Display: clean bar chart overview (terminal only) โ”€โ”€
  1310. if [ "$OUTPUT_MODE" = "terminal" ]; then
  1311. _dim_bar() {
  1312. local name="$1" score="$2" max="$3"
  1313. local gauge pct
  1314. if [ "$max" -le 0 ]; then pct=0
  1315. else pct=$(( (score * 100) / max )); fi
  1316. gauge=$(bar_gauge "$score" "$max" 15)
  1317. local colored_gauge
  1318. if [ "$pct" -ge 80 ]; then
  1319. colored_gauge=$(_green "$gauge")
  1320. elif [ "$pct" -ge 60 ]; then
  1321. colored_gauge=$(_yellow "$gauge")
  1322. else
  1323. colored_gauge=$(_red "$gauge")
  1324. fi
  1325. if [ "$score" -eq "$max" ]; then
  1326. printf " %-24s %s %2d / %-2d $(_green "โœ“")\n" "$name" "$colored_gauge" "$score" "$max"
  1327. else
  1328. printf " %-24s %s %2d / %-2d\n" "$name" "$colored_gauge" "$score" "$max"
  1329. fi
  1330. }
  1331. echo ""
  1332. echo " $(_bold "Score Overview:")"
  1333. echo ""
  1334. _dim_bar "Description Quality" "$desc_score" "30"
  1335. _dim_bar "Instruction Clarity" "$clarity_score" "25"
  1336. _dim_bar "Spec Compliance" "$spec_score" "20"
  1337. _dim_bar "Progressive Disclosure" "$progressive_score" "15"
  1338. _dim_bar "Security" "$security_score" "10"
  1339. echo ""
  1340. echo " $(_dim "$(printf '%.0sโ”€' $(seq 1 52))")"
  1341. local total_gauge
  1342. total_gauge=$(bar_gauge "$total_score" 100 20)
  1343. local colored_total
  1344. if [ "$total_score" -ge 85 ]; then
  1345. colored_total=$(_bold_green "$total_gauge")
  1346. elif [ "$total_score" -ge 65 ]; then
  1347. colored_total=$(_bold_yellow "$total_gauge")
  1348. else
  1349. colored_total=$(_bold_red "$total_gauge")
  1350. fi
  1351. printf " %-24s %s %d/100 Grade: %s\n" "$(_bold "TOTAL")" "$colored_total" "$total_score" "$(grade_color "$total_score")"
  1352. fi # end terminal-only display
  1353. # โ”€โ”€ Collect failed sub-checks with sources and actionable fixes โ”€โ”€
  1354. # Sources verified against:
  1355. # [spec] agentskills.io/specification
  1356. # [official] agentskills.io/skill-creation/best-practices
  1357. # agentskills.io/skill-creation/optimizing-descriptions
  1358. # [community] mdskills.ai/docs/skill-best-practices
  1359. local fix_count=0
  1360. local -a fix_labels=()
  1361. local -a fix_sources=()
  1362. local -a fix_dims=()
  1363. [ "$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"); }
  1364. [ "$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"); }
  1365. [ "$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"); }
  1366. [ "$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"); }
  1367. [ "$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"); }
  1368. [ "$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"); }
  1369. [ "$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"); }
  1370. [ "$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"); }
  1371. [ "$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"); }
  1372. [ "$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"); }
  1373. [ "$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"); }
  1374. [ "$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"); }
  1375. [ "$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"); }
  1376. [ "$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"); }
  1377. [ "$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"); }
  1378. [ "$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"); }
  1379. [ "$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"); }
  1380. [ "$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"); }
  1381. [ "$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"); }
  1382. [ "$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"); }
  1383. [ "$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"); }
  1384. [ "$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"); }
  1385. [ "$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"); }
  1386. # โ”€โ”€ Display: "How to Improve" section with sourced fixes (terminal only) โ”€โ”€
  1387. if [ "$OUTPUT_MODE" = "terminal" ]; then
  1388. if [ "$fix_count" -gt 0 ]; then
  1389. echo ""
  1390. echo ""
  1391. echo " $(_bold "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”")"
  1392. echo " $(_bold "โ”‚") $(_bold "HOW TO REACH 100/100") $(_dim "($fix_count items, by impact)") $(_bold "โ”‚")"
  1393. echo " $(_bold "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜")"
  1394. local prev_dim=""
  1395. local i
  1396. for ((i=0; i<fix_count; i++)); do
  1397. local dim="${fix_dims[$i]}"
  1398. local label="${fix_labels[$i]}"
  1399. local source="${fix_sources[$i]}"
  1400. if [ "$dim" != "$prev_dim" ]; then
  1401. echo ""
  1402. echo " $(_bold_cyan "$dim:")"
  1403. prev_dim="$dim"
  1404. fi
  1405. echo " $(_yellow "โ†’") $label"
  1406. echo " $(_dim "$source")"
  1407. done
  1408. echo ""
  1409. echo " $(_dim "Sources: spec = agentskills.io/specification")"
  1410. echo " $(_dim " official = agentskills.io/skill-creation/*")"
  1411. echo " $(_dim " community = mdskills.ai/docs/skill-best-practices")"
  1412. else
  1413. echo ""
  1414. _pass "Perfect score โ€” no improvements needed"
  1415. fi
  1416. fi
  1417. }
  1418. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  1419. # OUTPUT MODES
  1420. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  1421. print_json() {
  1422. local elapsed=$(( SECONDS - SCAN_START ))
  1423. # Collect findings into JSON arrays
  1424. local errors_json="[]" warnings_json="[]" info_json="[]"
  1425. local err_items=""
  1426. while IFS='|' read -r sev sec msg; do
  1427. [ -z "$sev" ] && continue
  1428. msg=$(echo "$msg" | sed 's/"/\\"/g')
  1429. sec=$(echo "$sec" | sed 's/"/\\"/g')
  1430. case "$sev" in
  1431. ERROR) err_items="${err_items}{\"section\":\"$sec\",\"message\":\"$msg\"}," ;;
  1432. esac
  1433. done < "$FINDINGS_FILE"
  1434. local warn_items=""
  1435. while IFS='|' read -r sev sec msg; do
  1436. [ -z "$sev" ] && continue
  1437. msg=$(echo "$msg" | sed 's/"/\\"/g')
  1438. sec=$(echo "$sec" | sed 's/"/\\"/g')
  1439. case "$sev" in
  1440. WARN) warn_items="${warn_items}{\"section\":\"$sec\",\"message\":\"$msg\"}," ;;
  1441. esac
  1442. done < "$FINDINGS_FILE"
  1443. local info_items=""
  1444. while IFS='|' read -r sev sec msg; do
  1445. [ -z "$sev" ] && continue
  1446. msg=$(echo "$msg" | sed 's/"/\\"/g')
  1447. sec=$(echo "$sec" | sed 's/"/\\"/g')
  1448. case "$sev" in
  1449. INFO) info_items="${info_items}{\"section\":\"$sec\",\"message\":\"$msg\"}," ;;
  1450. esac
  1451. done < "$FINDINGS_FILE"
  1452. # Strip trailing commas and wrap
  1453. err_items="${err_items%,}"
  1454. warn_items="${warn_items%,}"
  1455. info_items="${info_items%,}"
  1456. [ -n "$err_items" ] && errors_json="[$err_items]"
  1457. [ -n "$warn_items" ] && warnings_json="[$warn_items]"
  1458. [ -n "$info_items" ] && info_json="[$info_items]"
  1459. local result="PASS"
  1460. [ "$WARNINGS" -gt 0 ] && result="PASS_WITH_WARNINGS"
  1461. [ "$ERRORS" -gt 0 ] && result="FAIL"
  1462. cat <<ENDJSON
  1463. {
  1464. "version": "4.0",
  1465. "skill": "$(pwd)",
  1466. "timestamp": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
  1467. "duration_seconds": $elapsed,
  1468. "result": "$result",
  1469. "counts": {
  1470. "checks": $CHECKS_RUN,
  1471. "errors": $ERRORS,
  1472. "warnings": $WARNINGS,
  1473. "info": $INFO_COUNT
  1474. },
  1475. "quality_score": {
  1476. "total": $QUALITY_SCORE,
  1477. "grade": "$QUALITY_GRADE",
  1478. "dimensions": {
  1479. "description_quality": { "score": $QUALITY_DESC, "max": 30 },
  1480. "instruction_clarity": { "score": $QUALITY_CLARITY, "max": 25 },
  1481. "spec_compliance": { "score": $QUALITY_SPEC, "max": 20 },
  1482. "progressive_disclosure": { "score": $QUALITY_PROGRESSIVE, "max": 15 },
  1483. "security": { "score": $QUALITY_SECURITY, "max": 10 }
  1484. }
  1485. },
  1486. "token_analysis": {
  1487. "skill_md_tokens": $_S_BODY_TOKENS,
  1488. "body_lines": $_S_BODY_LINES,
  1489. "code_blocks": $_S_CODE_BLOCKS,
  1490. "headings": $_S_HEADINGS,
  1491. "reference_links": $_S_REF_LINKS,
  1492. "reference_files_total": $_S_REFS_TOTAL,
  1493. "reference_files_linked": $_S_REFS_LINKED
  1494. },
  1495. "findings": {
  1496. "errors": $errors_json,
  1497. "warnings": $warnings_json,
  1498. "info": $info_json
  1499. }
  1500. }
  1501. ENDJSON
  1502. }
  1503. print_markdown() {
  1504. local elapsed=$(( SECONDS - SCAN_START ))
  1505. local result_icon result_text
  1506. if [ "$ERRORS" -gt 0 ]; then
  1507. result_icon="x" result_text="FAIL โ€” $ERRORS error(s), $WARNINGS warning(s)"
  1508. elif [ "$WARNINGS" -gt 0 ]; then
  1509. result_icon="!" result_text="PASS with $WARNINGS warning(s)"
  1510. else
  1511. result_icon="+" result_text="All checks passed"
  1512. fi
  1513. cat <<ENDMD
  1514. # Agent Skill Scan Report
  1515. **Score: $QUALITY_SCORE/100 ($QUALITY_GRADE)** | Result: $result_text | Duration: ${elapsed}s
  1516. ## Score Breakdown
  1517. | Dimension | Score | Max | % |
  1518. |---|---|---|---|
  1519. | Description Quality | $QUALITY_DESC | 30 | $(( (QUALITY_DESC * 100) / 30 ))% |
  1520. | Instruction Clarity | $QUALITY_CLARITY | 25 | $(( (QUALITY_CLARITY * 100) / 25 ))% |
  1521. | Spec Compliance | $QUALITY_SPEC | 20 | $(( (QUALITY_SPEC * 100) / 20 ))% |
  1522. | Progressive Disclosure | $QUALITY_PROGRESSIVE | 15 | $(( (QUALITY_PROGRESSIVE * 100) / 15 ))% |
  1523. | Security | $QUALITY_SECURITY | 10 | $(( (QUALITY_SECURITY * 100) / 10 ))% |
  1524. | **Total** | **$QUALITY_SCORE** | **100** | **${QUALITY_SCORE}%** |
  1525. ## Summary
  1526. | Metric | Count |
  1527. |---|---|
  1528. | Sections checked | $CHECKS_RUN |
  1529. | Errors | $ERRORS |
  1530. | Warnings | $WARNINGS |
  1531. | Suggestions | $INFO_COUNT |
  1532. ## Token Analysis
  1533. | File | Tokens | Lines |
  1534. |---|---|---|
  1535. | SKILL.md | ~$_S_BODY_TOKENS | $_S_BODY_LINES |
  1536. | Reference files | $_S_REFS_TOTAL files | $_S_REFS_LINKED linked |
  1537. ENDMD
  1538. # Findings
  1539. local total_findings
  1540. total_findings=$(wc -l < "$FINDINGS_FILE" | tr -d ' ')
  1541. if [ "$total_findings" -gt 0 ]; then
  1542. echo ""
  1543. echo "## Findings"
  1544. echo ""
  1545. local has_errors has_warnings has_info
  1546. has_errors=$(grep -c "^ERROR|" "$FINDINGS_FILE" 2>/dev/null || true)
  1547. has_warnings=$(grep -c "^WARN|" "$FINDINGS_FILE" 2>/dev/null || true)
  1548. has_info=$(grep -c "^INFO|" "$FINDINGS_FILE" 2>/dev/null || true)
  1549. if [ "${has_errors:-0}" -gt 0 ]; then
  1550. echo "### Errors"
  1551. echo ""
  1552. grep "^ERROR|" "$FINDINGS_FILE" | while IFS='|' read -r _ sec msg; do
  1553. echo "- **$sec**: $msg"
  1554. done
  1555. echo ""
  1556. fi
  1557. if [ "${has_warnings:-0}" -gt 0 ]; then
  1558. echo "### Warnings"
  1559. echo ""
  1560. grep "^WARN|" "$FINDINGS_FILE" | while IFS='|' read -r _ sec msg; do
  1561. echo "- **$sec**: $msg"
  1562. done
  1563. echo ""
  1564. fi
  1565. if [ "${has_info:-0}" -gt 0 ]; then
  1566. echo "### Suggestions"
  1567. echo ""
  1568. grep "^INFO|" "$FINDINGS_FILE" | while IFS='|' read -r _ sec msg; do
  1569. echo "- **$sec**: $msg"
  1570. done
  1571. echo ""
  1572. fi
  1573. fi
  1574. echo "---"
  1575. echo ""
  1576. echo "_Validated against [agentskills.io/specification](https://agentskills.io/specification) | Scanner v4.0 | $(date '+%Y-%m-%d %H:%M:%S')_"
  1577. }
  1578. print_score_only() {
  1579. if [ "$NO_COLOR" = "1" ]; then
  1580. echo "$QUALITY_SCORE/100 $QUALITY_GRADE"
  1581. else
  1582. echo "$QUALITY_SCORE/100 $(grade_color "$QUALITY_SCORE")"
  1583. fi
  1584. [ "$ERRORS" -gt 0 ] && exit 1
  1585. exit 0
  1586. }
  1587. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  1588. # REPORT (terminal)
  1589. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  1590. print_report() {
  1591. echo ""
  1592. echo ""
  1593. local elapsed=$(( SECONDS - SCAN_START ))
  1594. _pad() {
  1595. local text="$1" width="$2"
  1596. local visible
  1597. visible=$(echo "$text" | sed 's/\x1b\[[0-9;]*m//g')
  1598. local pad_len=$(( width - ${#visible} ))
  1599. if [ "$pad_len" -lt 0 ]; then pad_len=0; fi
  1600. printf '%s%*s' "$text" "$pad_len" ""
  1601. }
  1602. local W=58
  1603. local line
  1604. line=$(printf '%.0sโ•' $(seq 1 $W))
  1605. local thin_line
  1606. thin_line=$(printf '%.0sโ”€' $(seq 1 $W))
  1607. # โ”€โ”€ Summary box โ”€โ”€
  1608. echo " $(_bold "โ•”${line}โ•—")"
  1609. echo " $(_bold "โ•‘")$(_pad "" $W)$(_bold "โ•‘")"
  1610. echo " $(_bold "โ•‘")$(_pad " AGENT SKILL SCAN REPORT " $W)$(_bold "โ•‘")"
  1611. echo " $(_bold "โ•‘")$(_pad " Scanner v4.0 " $W)$(_bold "โ•‘")"
  1612. echo " $(_bold "โ•‘")$(_pad "" $W)$(_bold "โ•‘")"
  1613. echo " $(_bold "โ• ${line}โ•ฃ")"
  1614. echo " โ•‘$(_pad "" $W)โ•‘"
  1615. echo " โ•‘ $(_pad "Checks run : $CHECKS_RUN sections" $(( W - 2 )))โ•‘"
  1616. echo " โ•‘ $(_pad "$(_red "Errors") : $ERRORS" $(( W - 2 )))โ•‘"
  1617. echo " โ•‘ $(_pad "$(_yellow "Warnings") : $WARNINGS" $(( W - 2 )))โ•‘"
  1618. echo " โ•‘ $(_pad "$(_blue "Info") : $INFO_COUNT" $(( W - 2 )))โ•‘"
  1619. echo " โ•‘ $(_pad "Duration : ${elapsed}s" $(( W - 2 )))โ•‘"
  1620. echo " โ•‘$(_pad "" $W)โ•‘"
  1621. echo " $(_bold "โ• ${line}โ•ฃ")"
  1622. echo " โ•‘$(_pad "" $W)โ•‘"
  1623. local result_text
  1624. if [ "$ERRORS" -gt 0 ]; then
  1625. result_text=$(_bold_red "FAIL")
  1626. elif [ "$WARNINGS" -gt 0 ]; then
  1627. result_text=$(_bold_yellow "PASS with warnings")
  1628. else
  1629. result_text=$(_bold_green "ALL CLEAR")
  1630. fi
  1631. echo " โ•‘ $(_pad "Result : $result_text" $(( W - 2 )))โ•‘"
  1632. echo " โ•‘ $(_pad "Quality Score : $QUALITY_SCORE/100 ($(grade_color "$QUALITY_SCORE"))" $(( W - 2 )))โ•‘"
  1633. echo " โ•‘$(_pad "" $W)โ•‘"
  1634. echo " $(_bold "โ• ${line}โ•ฃ")"
  1635. echo " โ•‘$(_pad "" $W)โ•‘"
  1636. echo " โ•‘ $(_pad "Description : $QUALITY_DESC/30" $(( W - 2 )))โ•‘"
  1637. echo " โ•‘ $(_pad "Clarity : $QUALITY_CLARITY/25" $(( W - 2 )))โ•‘"
  1638. echo " โ•‘ $(_pad "Spec : $QUALITY_SPEC/20" $(( W - 2 )))โ•‘"
  1639. echo " โ•‘ $(_pad "Progressive : $QUALITY_PROGRESSIVE/15" $(( W - 2 )))โ•‘"
  1640. echo " โ•‘ $(_pad "Security : $QUALITY_SECURITY/10" $(( W - 2 )))โ•‘"
  1641. echo " โ•‘$(_pad "" $W)โ•‘"
  1642. echo " $(_bold "โ•š${line}โ•")"
  1643. # โ”€โ”€ Detailed findings โ”€โ”€
  1644. local total_findings
  1645. total_findings=$(wc -l < "$FINDINGS_FILE" | tr -d ' ')
  1646. if [ "$total_findings" -gt 0 ]; then
  1647. echo ""
  1648. echo " $(_bold "โ”Œ${thin_line}โ”")"
  1649. echo " $(_bold "โ”‚")$(_pad " DETAILED FINDINGS " $W)$(_bold "โ”‚")"
  1650. echo " $(_bold "โ””${thin_line}โ”˜")"
  1651. _print_findings_by_severity() {
  1652. local severity="$1" label="$2" color_fn="$3"
  1653. local matches
  1654. matches=$(grep "^${severity}|" "$FINDINGS_FILE" 2>/dev/null || true)
  1655. [ -z "$matches" ] && return
  1656. local count
  1657. count=$(echo "$matches" | wc -l | tr -d ' ')
  1658. echo ""
  1659. echo " $($color_fn "$label ($count)")"
  1660. echo " $(_dim "$(printf '%.0sโ”€' $(seq 1 56))")"
  1661. local prev_section=""
  1662. while IFS='|' read -r _ sec msg; do
  1663. if [ "$sec" != "$prev_section" ]; then
  1664. echo ""
  1665. echo " $(_dim "[$sec]")"
  1666. prev_section="$sec"
  1667. fi
  1668. echo " $($color_fn "โ–ธ") $msg"
  1669. done <<< "$matches"
  1670. }
  1671. _print_findings_by_severity "ERROR" "ERRORS" "_red"
  1672. _print_findings_by_severity "WARN" "WARNINGS" "_yellow"
  1673. _print_findings_by_severity "INFO" "SUGGESTIONS" "_blue"
  1674. echo ""
  1675. echo " $(_dim "$(printf '%.0sโ”€' $(seq 1 60))")"
  1676. else
  1677. echo ""
  1678. echo " $(_bold_green "No findings โ€” skill package is in perfect shape.")"
  1679. fi
  1680. echo ""
  1681. echo " $(_dim "Validated against agentskills.io/specification")"
  1682. echo " $(_dim "Scored using skill-tools 5-dimension rubric (0-100)")"
  1683. echo ""
  1684. # โ”€โ”€ CI job summary โ”€โ”€
  1685. if [ "$CI" = "true" ]; then
  1686. {
  1687. echo "## Agent Skill Scan Report"
  1688. echo ""
  1689. if [ "$ERRORS" -gt 0 ]; then
  1690. echo "**FAIL** โ€” $ERRORS error(s), $WARNINGS warning(s)"
  1691. elif [ "$WARNINGS" -gt 0 ]; then
  1692. echo "**PASS with $WARNINGS warning(s)**"
  1693. else
  1694. echo "**All checks passed**"
  1695. fi
  1696. echo ""
  1697. echo "**Quality Score: $QUALITY_SCORE/100 (Grade: $QUALITY_GRADE)**"
  1698. echo ""
  1699. echo "| Metric | Count |"
  1700. echo "|--------|-------|"
  1701. echo "| Checks | $CHECKS_RUN |"
  1702. echo "| Errors | $ERRORS |"
  1703. echo "| Warnings | $WARNINGS |"
  1704. echo "| Info | $INFO_COUNT |"
  1705. echo "| Duration | ${elapsed}s |"
  1706. echo ""
  1707. echo "| Dimension | Score |"
  1708. echo "|-----------|-------|"
  1709. echo "| Description Quality | $QUALITY_DESC/30 |"
  1710. echo "| Instruction Clarity | $QUALITY_CLARITY/25 |"
  1711. echo "| Spec Compliance | $QUALITY_SPEC/20 |"
  1712. echo "| Progressive Disclosure | $QUALITY_PROGRESSIVE/15 |"
  1713. echo "| Security | $QUALITY_SECURITY/10 |"
  1714. echo "| **Total** | **$QUALITY_SCORE/100 ($QUALITY_GRADE)** |"
  1715. if [ "$total_findings" -gt 0 ]; then
  1716. echo ""
  1717. echo "### Findings"
  1718. echo ""
  1719. echo "<details>"
  1720. echo "<summary>Click to expand ($total_findings findings)</summary>"
  1721. echo ""
  1722. _ci_findings() {
  1723. local severity="$1" icon="$2"
  1724. local matches
  1725. matches=$(grep "^${severity}|" "$FINDINGS_FILE" 2>/dev/null || true)
  1726. [ -z "$matches" ] && return
  1727. while IFS='|' read -r _ sec msg; do
  1728. echo "- ${icon} **${sec}**: ${msg}"
  1729. done <<< "$matches"
  1730. }
  1731. _ci_findings "ERROR" "x"
  1732. _ci_findings "WARN" "!"
  1733. _ci_findings "INFO" "i"
  1734. echo ""
  1735. echo "</details>"
  1736. fi
  1737. echo ""
  1738. echo "_Validated against [agentskills.io/specification](https://agentskills.io/specification) | Score: $QUALITY_SCORE/100 ($QUALITY_GRADE)_"
  1739. } >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
  1740. fi
  1741. }
  1742. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  1743. # MAIN
  1744. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  1745. show_help() {
  1746. echo ""
  1747. echo " $(_bold "Agent Skill Scanner v4")"
  1748. echo ""
  1749. echo " Validates skill packages against the agentskills.io specification"
  1750. echo " and community best practices for AI agent skills."
  1751. echo " Computes a quality score (0-100) across 5 dimensions."
  1752. echo ""
  1753. echo " $(_bold "Usage:")"
  1754. echo " ./scripts/validate.sh Run full scan (terminal)"
  1755. echo " ./scripts/validate.sh --json Output as JSON"
  1756. echo " ./scripts/validate.sh --md Output as Markdown report"
  1757. echo " ./scripts/validate.sh --score-only Print score and grade only"
  1758. echo " ./scripts/validate.sh --help Show this help"
  1759. echo ""
  1760. echo " $(_bold "Output Modes:")"
  1761. echo " $(_cyan "--json") Machine-readable JSON (pipe to jq, feed to web tools)"
  1762. echo " $(_cyan "--md") Markdown report (save to file, paste in PRs/docs)"
  1763. echo " $(_cyan "--score-only") Quick score check (e.g., in pre-commit hooks)"
  1764. echo " $(_dim "(default)") Rich terminal output with colors and bar charts"
  1765. echo ""
  1766. echo " $(_bold "Environment:")"
  1767. echo " CI=true Emit GitHub Actions annotations + job summary"
  1768. echo " NO_COLOR=1 Disable colored output"
  1769. echo ""
  1770. echo " $(_bold "Checks (15 sections, 20+ individual checks):")"
  1771. echo " $(_cyan " 1.") Skill structure Required/optional files and directories"
  1772. echo " $(_cyan " 2.") Frontmatter & desc name, description, voice, verbs, negative triggers"
  1773. echo " $(_cyan " 3.") Body & disclosure Line count, token budget, structure, code examples"
  1774. echo " $(_cyan " 4.") Internal links All links resolve to existing files"
  1775. echo " $(_cyan " 5.") Reference files Linked, orphaned, empty, and missing detection"
  1776. echo " $(_cyan " 6.") Markdown syntax Unclosed code blocks"
  1777. echo " $(_cyan " 7.") Reference nesting No deep cross-reference chains"
  1778. echo " $(_cyan " 8.") Scripts Executable permissions, documentation"
  1779. echo " $(_cyan " 9.") Repository hygiene Sensitive files, development artifacts"
  1780. echo " $(_cyan "10.") Token budget Per-file and total token analysis with zones"
  1781. echo " $(_cyan "11.") Content quality Code examples, headings, keyword density"
  1782. echo " $(_cyan "12.") Agent metadata agents/openai.yaml validation"
  1783. echo " $(_cyan "13.") Security scan API keys, secrets, hardcoded paths, dangerous commands"
  1784. echo " $(_cyan "14.") Heading hierarchy Level skip detection (MD001), duplicate headings (MD024)"
  1785. echo " $(_cyan "15.") Quality score 0-100 score across 5 dimensions with letter grade"
  1786. echo ""
  1787. echo " $(_bold "Quality Score Dimensions:")"
  1788. echo " Description Quality (30 pts) Length, specificity, voice, verbs, triggers"
  1789. echo " Instruction Clarity (25 pts) Code blocks, headings, reference links"
  1790. echo " Spec Compliance (20 pts) Required fields, name format, token/line limits"
  1791. echo " Progressive Discl. (15 pts) References dir, linking, body size"
  1792. echo " Security (10 pts) No secrets, paths, or dangerous commands"
  1793. echo ""
  1794. echo " $(_bold "Letter Grades:")"
  1795. echo " $(_bold_green "A+") 95+ $(_bold_green "A") 90+ $(_bold_green "A-") 85+ $(_bold_yellow "B+") 80+ $(_bold_yellow "B") 75+"
  1796. 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"
  1797. echo ""
  1798. echo " $(_bold "Token Zones:")"
  1799. echo " $(_green "[SAFE]") Well within budget, efficient"
  1800. echo " $(_yellow "[WARN]") At or near recommended limits"
  1801. echo " $(_red "[HIGH]") Exceeding recommendations, consider optimization"
  1802. echo ""
  1803. echo " $(_bold "Token Estimation:")"
  1804. echo " Uses chars/4 approximation (industry standard for tiktoken cl100k_base)"
  1805. echo " SKILL.md budget: <5000 tokens | Reference: <4000 tokens each"
  1806. echo " Total package: <50000 tokens recommended"
  1807. echo ""
  1808. echo " $(_bold "Severity:")"
  1809. echo " $(_red "โœ— ERROR") Spec violation or broken content โ€” must fix"
  1810. echo " $(_yellow "! WARN ") Best practice issue โ€” should fix"
  1811. echo " $(_blue "โ„น INFO ") Suggestion โ€” nice to have"
  1812. echo " $(_green "โœ“ PASS ") Check passed"
  1813. echo ""
  1814. echo " $(_bold "References:")"
  1815. echo " Spec: https://agentskills.io/specification"
  1816. echo " Scoring: https://agentskills.io/skill-creation/best-practices"
  1817. echo " Tokens: https://agentpatterns.ai/context-engineering/context-budget-allocation/"
  1818. echo ""
  1819. }
  1820. _suppress_terminal() {
  1821. [ "$OUTPUT_MODE" != "terminal" ]
  1822. }
  1823. main() {
  1824. # Parse CLI flags
  1825. while [ $# -gt 0 ]; do
  1826. case "$1" in
  1827. --help|-h) show_help; exit 0 ;;
  1828. --json) OUTPUT_MODE="json"; NO_COLOR=1 ;;
  1829. --md|--markdown) OUTPUT_MODE="markdown"; NO_COLOR=1 ;;
  1830. --score-only|--score) OUTPUT_MODE="score-only" ;;
  1831. *) echo "Unknown flag: $1"; echo "Run with --help for usage."; exit 2 ;;
  1832. esac
  1833. shift
  1834. done
  1835. if ! _suppress_terminal; then
  1836. echo ""
  1837. echo " $(_bold "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—")"
  1838. echo " $(_bold "โ•‘") $(_bold "โ•‘")"
  1839. echo " $(_bold "โ•‘") $(_bold_cyan "A G E N T S K I L L S C A N N E R") $(_bold "โ•‘")"
  1840. echo " $(_bold "โ•‘") $(_dim "v 4 . 0") $(_bold "โ•‘")"
  1841. echo " $(_bold "โ•‘") $(_bold "โ•‘")"
  1842. echo " $(_bold "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")"
  1843. echo ""
  1844. if [ "$CI" = "true" ]; then
  1845. _detail "Mode : CI (GitHub Actions annotations + job summary)"
  1846. else
  1847. _detail "Mode : Local"
  1848. fi
  1849. _detail "Skill: $(pwd)"
  1850. _detail "Spec : agentskills.io/specification"
  1851. _detail "Date : $(date '+%Y-%m-%d %H:%M:%S')"
  1852. echo ""
  1853. echo " $(_dim "Running $TOTAL_CHECKS sections with 20+ individual checks...")"
  1854. fi
  1855. if [ "$OUTPUT_MODE" != "terminal" ]; then
  1856. exec 3>&1 1>/dev/null
  1857. fi
  1858. check_structure
  1859. check_frontmatter
  1860. check_body
  1861. check_links
  1862. check_references
  1863. check_markdown
  1864. check_reference_depth
  1865. check_scripts
  1866. check_repo_hygiene
  1867. check_token_budget
  1868. check_content_quality
  1869. check_agents_metadata
  1870. check_security
  1871. check_heading_hierarchy
  1872. compute_quality_score
  1873. if [ "$OUTPUT_MODE" != "terminal" ]; then
  1874. exec 1>&3 3>&-
  1875. fi
  1876. case "$OUTPUT_MODE" in
  1877. json) print_json ;;
  1878. markdown) print_markdown ;;
  1879. score-only) print_score_only ;;
  1880. *) print_report ;;
  1881. esac
  1882. [ "$ERRORS" -gt 0 ] && exit 1
  1883. exit 0
  1884. }
  1885. main "$@"