@softspark/ai-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/AGENTS.md +412 -0
  2. package/CHANGELOG.md +68 -0
  3. package/LICENSE +21 -0
  4. package/README.md +632 -0
  5. package/action.yml +53 -0
  6. package/app/.claude-plugin/plugin.json +44 -0
  7. package/app/ARCHITECTURE.md +306 -0
  8. package/app/CLAUDE.md.template +23 -0
  9. package/app/agents/ai-engineer.md +128 -0
  10. package/app/agents/backend-specialist.md +193 -0
  11. package/app/agents/business-intelligence.md +54 -0
  12. package/app/agents/chaos-monkey.md +67 -0
  13. package/app/agents/chief-of-staff.md +51 -0
  14. package/app/agents/code-archaeologist.md +127 -0
  15. package/app/agents/code-reviewer.md +184 -0
  16. package/app/agents/command-expert.md +131 -0
  17. package/app/agents/data-analyst.md +205 -0
  18. package/app/agents/data-scientist.md +151 -0
  19. package/app/agents/database-architect.md +317 -0
  20. package/app/agents/debugger.md +238 -0
  21. package/app/agents/devops-implementer.md +194 -0
  22. package/app/agents/documenter.md +364 -0
  23. package/app/agents/explorer-agent.md +145 -0
  24. package/app/agents/fact-checker.md +172 -0
  25. package/app/agents/frontend-specialist.md +209 -0
  26. package/app/agents/game-developer.md +216 -0
  27. package/app/agents/incident-responder.md +226 -0
  28. package/app/agents/infrastructure-architect.md +127 -0
  29. package/app/agents/infrastructure-validator.md +247 -0
  30. package/app/agents/llm-ops-engineer.md +237 -0
  31. package/app/agents/mcp-expert.md +228 -0
  32. package/app/agents/mcp-server-architect.md +195 -0
  33. package/app/agents/mcp-testing-engineer.md +292 -0
  34. package/app/agents/meta-architect.md +58 -0
  35. package/app/agents/ml-engineer.md +136 -0
  36. package/app/agents/mobile-developer.md +190 -0
  37. package/app/agents/night-watchman.md +55 -0
  38. package/app/agents/nlp-engineer.md +154 -0
  39. package/app/agents/orchestrator.md +437 -0
  40. package/app/agents/performance-optimizer.md +254 -0
  41. package/app/agents/predictive-analyst.md +57 -0
  42. package/app/agents/product-manager.md +194 -0
  43. package/app/agents/project-planner.md +287 -0
  44. package/app/agents/prompt-engineer.md +103 -0
  45. package/app/agents/qa-automation-engineer.md +182 -0
  46. package/app/agents/rag-engineer.md +201 -0
  47. package/app/agents/research-synthesizer.md +138 -0
  48. package/app/agents/search-specialist.md +101 -0
  49. package/app/agents/security-architect.md +62 -0
  50. package/app/agents/security-auditor.md +293 -0
  51. package/app/agents/seo-specialist.md +111 -0
  52. package/app/agents/system-governor.md +57 -0
  53. package/app/agents/tech-lead.md +62 -0
  54. package/app/agents/technical-researcher.md +103 -0
  55. package/app/agents/test-engineer.md +264 -0
  56. package/app/constitution.md +38 -0
  57. package/app/hooks/_profile-check.sh +11 -0
  58. package/app/hooks/guard-destructive.sh +74 -0
  59. package/app/hooks/guard-path.sh +73 -0
  60. package/app/hooks/post-tool-use.sh +35 -0
  61. package/app/hooks/pre-compact.sh +31 -0
  62. package/app/hooks/quality-check.sh +22 -0
  63. package/app/hooks/quality-gate.sh +49 -0
  64. package/app/hooks/save-session.sh +24 -0
  65. package/app/hooks/session-end.sh +37 -0
  66. package/app/hooks/session-start.sh +29 -0
  67. package/app/hooks/subagent-start.sh +16 -0
  68. package/app/hooks/subagent-stop.sh +16 -0
  69. package/app/hooks/track-usage.sh +50 -0
  70. package/app/hooks/user-prompt-submit.sh +25 -0
  71. package/app/hooks.json +178 -0
  72. package/app/mcp-defaults.json +23 -0
  73. package/app/output-styles/golden-rules.md +43 -0
  74. package/app/plugins/README.md +19 -0
  75. package/app/plugins/csharp-pack/README.md +11 -0
  76. package/app/plugins/csharp-pack/plugin.json +18 -0
  77. package/app/plugins/enterprise-pack/README.md +16 -0
  78. package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
  79. package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
  80. package/app/plugins/enterprise-pack/plugin.json +24 -0
  81. package/app/plugins/frontend-pack/README.md +14 -0
  82. package/app/plugins/frontend-pack/plugin.json +22 -0
  83. package/app/plugins/java-pack/README.md +11 -0
  84. package/app/plugins/java-pack/plugin.json +18 -0
  85. package/app/plugins/kotlin-pack/README.md +11 -0
  86. package/app/plugins/kotlin-pack/plugin.json +18 -0
  87. package/app/plugins/memory-pack/README.md +24 -0
  88. package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
  89. package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
  90. package/app/plugins/memory-pack/plugin.json +22 -0
  91. package/app/plugins/memory-pack/scripts/init_db.py +81 -0
  92. package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
  93. package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
  94. package/app/plugins/research-pack/README.md +14 -0
  95. package/app/plugins/research-pack/plugin.json +22 -0
  96. package/app/plugins/ruby-pack/README.md +11 -0
  97. package/app/plugins/ruby-pack/plugin.json +18 -0
  98. package/app/plugins/rust-pack/README.md +11 -0
  99. package/app/plugins/rust-pack/plugin.json +18 -0
  100. package/app/plugins/security-pack/README.md +15 -0
  101. package/app/plugins/security-pack/plugin.json +23 -0
  102. package/app/plugins/swift-pack/README.md +11 -0
  103. package/app/plugins/swift-pack/plugin.json +18 -0
  104. package/app/rules/claude-toolkit-rules.md +21 -0
  105. package/app/rules/git-conventions.md +5 -0
  106. package/app/rules/quality-gates.md +10 -0
  107. package/app/skills/_lib/__init__.py +1 -0
  108. package/app/skills/_lib/detect_utils.py +150 -0
  109. package/app/skills/agent-creator/SKILL.md +82 -0
  110. package/app/skills/analyze/SKILL.md +92 -0
  111. package/app/skills/analyze/scripts/complexity.py +165 -0
  112. package/app/skills/api-patterns/SKILL.md +305 -0
  113. package/app/skills/app-builder/SKILL.md +187 -0
  114. package/app/skills/architecture-audit/SKILL.md +141 -0
  115. package/app/skills/architecture-decision/SKILL.md +55 -0
  116. package/app/skills/architecture-decision/templates/adr-template.md +36 -0
  117. package/app/skills/biz-scan/SKILL.md +30 -0
  118. package/app/skills/briefing/SKILL.md +27 -0
  119. package/app/skills/build/SKILL.md +97 -0
  120. package/app/skills/build/scripts/detect-build.py +151 -0
  121. package/app/skills/chaos/SKILL.md +32 -0
  122. package/app/skills/ci/SKILL.md +77 -0
  123. package/app/skills/ci/scripts/ci-detect.py +135 -0
  124. package/app/skills/ci/templates/github-actions-node.yml +38 -0
  125. package/app/skills/ci/templates/github-actions-python.yml +42 -0
  126. package/app/skills/ci-cd-patterns/SKILL.md +299 -0
  127. package/app/skills/clean-code/SKILL.md +110 -0
  128. package/app/skills/clean-code/reference/dart.md +18 -0
  129. package/app/skills/clean-code/reference/go.md +23 -0
  130. package/app/skills/clean-code/reference/php.md +32 -0
  131. package/app/skills/clean-code/reference/python.md +180 -0
  132. package/app/skills/clean-code/reference/typescript.md +26 -0
  133. package/app/skills/command-creator/SKILL.md +83 -0
  134. package/app/skills/commit/SKILL.md +98 -0
  135. package/app/skills/commit/scripts/pre-commit-check.py +87 -0
  136. package/app/skills/commit/templates/conventional-commit.md +52 -0
  137. package/app/skills/csharp-patterns/SKILL.md +450 -0
  138. package/app/skills/database-patterns/SKILL.md +297 -0
  139. package/app/skills/debug/SKILL.md +154 -0
  140. package/app/skills/debug/scripts/error-parser.py +187 -0
  141. package/app/skills/debugging-tactics/SKILL.md +136 -0
  142. package/app/skills/deploy/SKILL.md +130 -0
  143. package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
  144. package/app/skills/deploy/templates/deployment-checklist.md +31 -0
  145. package/app/skills/design-an-interface/SKILL.md +105 -0
  146. package/app/skills/design-engineering/SKILL.md +260 -0
  147. package/app/skills/docker-devops/SKILL.md +303 -0
  148. package/app/skills/docs/SKILL.md +145 -0
  149. package/app/skills/docs/scripts/doc-inventory.py +176 -0
  150. package/app/skills/docs/templates/adr-template.md +36 -0
  151. package/app/skills/docs/templates/readme-template.md +67 -0
  152. package/app/skills/documentation-standards/SKILL.md +191 -0
  153. package/app/skills/ecommerce-patterns/SKILL.md +209 -0
  154. package/app/skills/evaluate/SKILL.md +132 -0
  155. package/app/skills/evolve/SKILL.md +27 -0
  156. package/app/skills/explain/SKILL.md +54 -0
  157. package/app/skills/explain/scripts/dependency-graph.py +215 -0
  158. package/app/skills/explore/SKILL.md +112 -0
  159. package/app/skills/explore/scripts/visualize.py +117 -0
  160. package/app/skills/fix/SKILL.md +78 -0
  161. package/app/skills/fix/scripts/error-classifier.py +191 -0
  162. package/app/skills/flutter-patterns/SKILL.md +254 -0
  163. package/app/skills/git-mastery/SKILL.md +70 -0
  164. package/app/skills/grill-me/SKILL.md +38 -0
  165. package/app/skills/health/SKILL.md +91 -0
  166. package/app/skills/health/scripts/health_check.py +162 -0
  167. package/app/skills/hive-mind/SKILL.md +56 -0
  168. package/app/skills/hook-creator/SKILL.md +107 -0
  169. package/app/skills/index/SKILL.md +74 -0
  170. package/app/skills/instinct-review/SKILL.md +77 -0
  171. package/app/skills/java-patterns/SKILL.md +442 -0
  172. package/app/skills/kotlin-patterns/SKILL.md +446 -0
  173. package/app/skills/lint/SKILL.md +103 -0
  174. package/app/skills/lint/scripts/detect-linters.py +112 -0
  175. package/app/skills/mcp-patterns/SKILL.md +270 -0
  176. package/app/skills/mem-search/SKILL.md +70 -0
  177. package/app/skills/migrate/SKILL.md +90 -0
  178. package/app/skills/migrate/scripts/migration-status.py +195 -0
  179. package/app/skills/migration-patterns/SKILL.md +260 -0
  180. package/app/skills/night-watch/SKILL.md +28 -0
  181. package/app/skills/observability-patterns/SKILL.md +203 -0
  182. package/app/skills/onboard/SKILL.md +76 -0
  183. package/app/skills/orchestrate/SKILL.md +86 -0
  184. package/app/skills/panic/SKILL.md +30 -0
  185. package/app/skills/performance-profiling/SKILL.md +59 -0
  186. package/app/skills/plan/SKILL.md +110 -0
  187. package/app/skills/plan/templates/plan-template.md +40 -0
  188. package/app/skills/plan-writing/SKILL.md +201 -0
  189. package/app/skills/plugin-creator/SKILL.md +78 -0
  190. package/app/skills/pr/SKILL.md +129 -0
  191. package/app/skills/pr/scripts/pr-summary.py +175 -0
  192. package/app/skills/prd-to-issues/SKILL.md +108 -0
  193. package/app/skills/prd-to-plan/SKILL.md +120 -0
  194. package/app/skills/predict/SKILL.md +30 -0
  195. package/app/skills/qa-session/SKILL.md +110 -0
  196. package/app/skills/rag-patterns/SKILL.md +203 -0
  197. package/app/skills/refactor/SKILL.md +124 -0
  198. package/app/skills/refactor/scripts/refactor-scan.py +210 -0
  199. package/app/skills/refactor-plan/SKILL.md +112 -0
  200. package/app/skills/repeat/SKILL.md +149 -0
  201. package/app/skills/research-mastery/SKILL.md +56 -0
  202. package/app/skills/review/SKILL.md +141 -0
  203. package/app/skills/review/scripts/diff-analyzer.py +170 -0
  204. package/app/skills/rollback/SKILL.md +87 -0
  205. package/app/skills/rollback/scripts/rollback_info.py +149 -0
  206. package/app/skills/ruby-patterns/SKILL.md +454 -0
  207. package/app/skills/rust-patterns/SKILL.md +446 -0
  208. package/app/skills/search/SKILL.md +64 -0
  209. package/app/skills/security-patterns/SKILL.md +91 -0
  210. package/app/skills/security-patterns/reference/authentication.md +37 -0
  211. package/app/skills/security-patterns/reference/authorization.md +22 -0
  212. package/app/skills/security-patterns/reference/input-validation.md +30 -0
  213. package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
  214. package/app/skills/skill-creator/SKILL.md +154 -0
  215. package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
  216. package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
  217. package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
  218. package/app/skills/subagent-development/SKILL.md +225 -0
  219. package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
  220. package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
  221. package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
  222. package/app/skills/swarm/SKILL.md +81 -0
  223. package/app/skills/swift-patterns/SKILL.md +500 -0
  224. package/app/skills/tdd/SKILL.md +174 -0
  225. package/app/skills/tdd/reference/deep-modules.md +32 -0
  226. package/app/skills/tdd/reference/interface-design.md +32 -0
  227. package/app/skills/tdd/reference/mocking.md +52 -0
  228. package/app/skills/tdd/reference/refactoring.md +10 -0
  229. package/app/skills/tdd/reference/tests.md +59 -0
  230. package/app/skills/teams/SKILL.md +101 -0
  231. package/app/skills/test/SKILL.md +107 -0
  232. package/app/skills/test/scripts/detect-runner.py +113 -0
  233. package/app/skills/testing-patterns/SKILL.md +73 -0
  234. package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
  235. package/app/skills/testing-patterns/reference/go-testing.md +52 -0
  236. package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
  237. package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
  238. package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
  239. package/app/skills/triage-issue/SKILL.md +120 -0
  240. package/app/skills/typescript-patterns/SKILL.md +256 -0
  241. package/app/skills/ubiquitous-language/SKILL.md +74 -0
  242. package/app/skills/verification-before-completion/SKILL.md +108 -0
  243. package/app/skills/workflow/SKILL.md +250 -0
  244. package/app/skills/write-a-prd/SKILL.md +129 -0
  245. package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
  246. package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
  247. package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
  248. package/app/templates/skill/generator/SKILL.md.template +40 -0
  249. package/app/templates/skill/knowledge/SKILL.md.template +52 -0
  250. package/app/templates/skill/linter/SKILL.md.template +34 -0
  251. package/app/templates/skill/reviewer/SKILL.md.template +51 -0
  252. package/app/templates/skill/workflow/SKILL.md.template +49 -0
  253. package/benchmarks/README.md +111 -0
  254. package/benchmarks/ecosystem-dashboard.json +148 -0
  255. package/benchmarks/ecosystem-harvest.json +148 -0
  256. package/benchmarks/results.json +38 -0
  257. package/benchmarks/run.py +351 -0
  258. package/bin/ai-toolkit.js +345 -0
  259. package/kb/best-practices/README.md +11 -0
  260. package/kb/howto/README.md +11 -0
  261. package/kb/procedures/maintenance-sop.md +306 -0
  262. package/kb/reference/agents-catalog.md +124 -0
  263. package/kb/reference/anti-pattern-registry-format.md +221 -0
  264. package/kb/reference/architecture-overview.md +232 -0
  265. package/kb/reference/benchmark-config.md +62 -0
  266. package/kb/reference/ci-integration.md +66 -0
  267. package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
  268. package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
  269. package/kb/reference/commands-catalog.md +21 -0
  270. package/kb/reference/distribution-model.md +63 -0
  271. package/kb/reference/global-install-model.md +56 -0
  272. package/kb/reference/hierarchical-override-pattern.md +200 -0
  273. package/kb/reference/hooks-catalog.md +306 -0
  274. package/kb/reference/integrations.md +88 -0
  275. package/kb/reference/language-packs.md +52 -0
  276. package/kb/reference/merge-friendly-install-model.md +58 -0
  277. package/kb/reference/plugin-pack-conventions.md +151 -0
  278. package/kb/reference/quick-wins-implementation-summary.md +70 -0
  279. package/kb/reference/skill-templates.md +50 -0
  280. package/kb/reference/skills-catalog.md +215 -0
  281. package/kb/reference/skills-unification.md +57 -0
  282. package/kb/reference/stats.md +69 -0
  283. package/kb/reference/sync.md +76 -0
  284. package/kb/troubleshooting/README.md +11 -0
  285. package/llms-full.txt +3068 -0
  286. package/llms.txt +39 -0
  287. package/package.json +75 -0
  288. package/scripts/_common.py +160 -0
  289. package/scripts/add_rule.py +50 -0
  290. package/scripts/benchmark_config.py +127 -0
  291. package/scripts/benchmark_ecosystem.py +288 -0
  292. package/scripts/check_deps.py +260 -0
  293. package/scripts/create_skill.py +118 -0
  294. package/scripts/doctor.py +504 -0
  295. package/scripts/eject.py +113 -0
  296. package/scripts/emission.py +256 -0
  297. package/scripts/evaluate_skills.py +260 -0
  298. package/scripts/frontmatter.py +58 -0
  299. package/scripts/generate_agents_md.py +91 -0
  300. package/scripts/generate_aider_conf.py +51 -0
  301. package/scripts/generate_cline.py +35 -0
  302. package/scripts/generate_copilot.py +30 -0
  303. package/scripts/generate_cursor_rules.py +35 -0
  304. package/scripts/generate_gemini.py +28 -0
  305. package/scripts/generate_llms_txt.py +164 -0
  306. package/scripts/generate_roo_modes.py +80 -0
  307. package/scripts/generate_windsurf.py +35 -0
  308. package/scripts/generator_base.py +140 -0
  309. package/scripts/harvest_ecosystem.py +50 -0
  310. package/scripts/inject_rule_cli.py +101 -0
  311. package/scripts/inject_section_cli.py +47 -0
  312. package/scripts/injection.py +180 -0
  313. package/scripts/install.py +236 -0
  314. package/scripts/install_git_hooks.py +71 -0
  315. package/scripts/install_steps/__init__.py +5 -0
  316. package/scripts/install_steps/ai_tools.py +261 -0
  317. package/scripts/install_steps/hooks.py +90 -0
  318. package/scripts/install_steps/markers.py +79 -0
  319. package/scripts/install_steps/symlinks.py +87 -0
  320. package/scripts/merge-hooks.py +192 -0
  321. package/scripts/plugin.py +642 -0
  322. package/scripts/plugin_schema.py +138 -0
  323. package/scripts/remove_rule.py +58 -0
  324. package/scripts/stats.py +81 -0
  325. package/scripts/sync.py +215 -0
  326. package/scripts/uninstall.py +292 -0
  327. package/scripts/validate.py +700 -0
@@ -0,0 +1,700 @@
1
+ #!/usr/bin/env python3
2
+ """AI Toolkit Validator.
3
+
4
+ Checks all agent and skill files for correctness, validates hook events,
5
+ planned assets, plugin packs, KB documents, metadata contracts, and
6
+ content quality.
7
+
8
+ Usage:
9
+ python3 scripts/validate.py [--strict] [toolkit-dir]
10
+
11
+ Exit codes:
12
+ 0 validation passed
13
+ 1 validation failed (errors found, or warnings in --strict mode)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
23
+ from _common import toolkit_dir as default_toolkit_dir, frontmatter_field
24
+ from plugin_schema import validate_manifest as _validate_plugin_manifest_schema
25
+ from plugin_schema import validate_references as _validate_plugin_references
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Constants
30
+ # ---------------------------------------------------------------------------
31
+
32
+ VALID_TOOLS = frozenset({
33
+ "Read", "Write", "Edit", "Bash", "Grep", "Glob", "Agent",
34
+ "WebSearch", "WebFetch", "TodoRead", "TodoWrite",
35
+ "TeamCreate", "TeamDelete", "SendMessage",
36
+ "TaskCreate", "TaskList", "TaskUpdate", "TaskGet", "TaskOutput", "TaskStop",
37
+ "NotebookEdit", "ExitPlanMode", "EnterPlanMode",
38
+ "ExitWorktree", "EnterWorktree", "RemoteTrigger",
39
+ })
40
+
41
+ VALID_HOOK_EVENTS = frozenset({
42
+ "SessionStart", "Notification", "PreToolUse", "PostToolUse", "Stop",
43
+ "PreCompact", "SubagentStop", "UserPromptSubmit", "TaskCompleted",
44
+ "TeammateIdle", "SubagentStart", "SessionEnd", "PermissionRequest", "Setup",
45
+ })
46
+
47
+ VALID_KB_CATEGORIES = frozenset({
48
+ "reference", "howto", "procedures", "troubleshooting", "best-practices",
49
+ })
50
+
51
+ PLANNED_ASSETS = [
52
+ "app/.claude-plugin/plugin.json",
53
+ "scripts/doctor.py",
54
+ "scripts/benchmark_ecosystem.py",
55
+ "scripts/harvest_ecosystem.py",
56
+ "app/hooks/pre-compact.sh",
57
+ "app/hooks/post-tool-use.sh",
58
+ "app/hooks/user-prompt-submit.sh",
59
+ "app/hooks/subagent-start.sh",
60
+ "app/hooks/subagent-stop.sh",
61
+ "app/hooks/session-end.sh",
62
+ "app/hooks/track-usage.sh",
63
+ "app/skills/hook-creator/SKILL.md",
64
+ "app/skills/command-creator/SKILL.md",
65
+ "app/skills/agent-creator/SKILL.md",
66
+ "app/skills/plugin-creator/SKILL.md",
67
+ "kb/reference/claude-ecosystem-benchmark-snapshot.md",
68
+ "kb/reference/plugin-pack-conventions.md",
69
+ "benchmarks/ecosystem-dashboard.json",
70
+ ]
71
+
72
+ CORE_FILES = [
73
+ "app/constitution.md",
74
+ "app/ARCHITECTURE.md",
75
+ "scripts/install.py",
76
+ "app/hooks.json",
77
+ "README.md",
78
+ ]
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Frontmatter helpers
83
+ # ---------------------------------------------------------------------------
84
+
85
+ def _parse_frontmatter_lines(filepath: Path) -> list[str]:
86
+ """Return frontmatter lines (between --- delimiters), excluding delimiters."""
87
+ lines: list[str] = []
88
+ in_fm = False
89
+ with open(filepath, encoding="utf-8") as f:
90
+ for line in f:
91
+ stripped = line.rstrip("\n")
92
+ if stripped == "---":
93
+ if in_fm:
94
+ break
95
+ in_fm = True
96
+ continue
97
+ if in_fm:
98
+ lines.append(stripped)
99
+ return lines
100
+
101
+
102
+ def _has_frontmatter(filepath: Path) -> bool:
103
+ """Check if file starts with ---."""
104
+ with open(filepath, encoding="utf-8") as f:
105
+ first_line = f.readline().rstrip("\n")
106
+ return first_line == "---"
107
+
108
+
109
+ def _fm_field(lines: list[str], field: str) -> str:
110
+ """Extract a field value from frontmatter lines."""
111
+ for line in lines:
112
+ if line.startswith(f"{field}:"):
113
+ value = line[len(field) + 1:].strip()
114
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
115
+ value = value[1:-1]
116
+ return value
117
+ return ""
118
+
119
+
120
+ def _fm_has(lines: list[str], field: str) -> bool:
121
+ """Check if frontmatter has a field."""
122
+ return any(line.startswith(f"{field}:") for line in lines)
123
+
124
+
125
+ def _body_line_count(filepath: Path) -> int:
126
+ """Count lines after the second --- delimiter."""
127
+ count = 0
128
+ delimiters_seen = 0
129
+ with open(filepath, encoding="utf-8") as f:
130
+ for line in f:
131
+ stripped = line.rstrip("\n")
132
+ if stripped == "---":
133
+ delimiters_seen += 1
134
+ continue
135
+ if delimiters_seen >= 2:
136
+ count += 1
137
+ return count
138
+
139
+
140
+ def _body_nonblank_count(filepath: Path) -> int:
141
+ """Count non-blank lines after the second --- delimiter."""
142
+ count = 0
143
+ delimiters_seen = 0
144
+ with open(filepath, encoding="utf-8") as f:
145
+ for line in f:
146
+ stripped = line.rstrip("\n")
147
+ if stripped == "---":
148
+ delimiters_seen += 1
149
+ continue
150
+ if delimiters_seen >= 2 and stripped.strip():
151
+ count += 1
152
+ return count
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Validation sections
157
+ # ---------------------------------------------------------------------------
158
+
159
+ class ValidationResult:
160
+ """Accumulates errors and warnings."""
161
+
162
+ def __init__(self) -> None:
163
+ self.errors = 0
164
+ self.warnings = 0
165
+
166
+ def error(self, msg: str) -> None:
167
+ print(f" ERROR: {msg}")
168
+ self.errors += 1
169
+
170
+ def warn(self, msg: str) -> None:
171
+ print(f" WARNING: {msg}")
172
+ self.warnings += 1
173
+
174
+
175
+ def validate_agents(tk_dir: Path, vr: ValidationResult) -> int:
176
+ """Validate agent .md files. Returns agent count."""
177
+ print("## Agents")
178
+ agents_dir = tk_dir / "app" / "agents"
179
+ agent_count = 0
180
+
181
+ if not agents_dir.is_dir():
182
+ vr.error("app/agents directory not found")
183
+ print()
184
+ return 0
185
+
186
+ for filepath in sorted(agents_dir.glob("*.md")):
187
+ agent_count += 1
188
+ name = filepath.stem
189
+
190
+ if not _has_frontmatter(filepath):
191
+ vr.error(f"{name} - Missing YAML frontmatter")
192
+ continue
193
+
194
+ fm_lines = _parse_frontmatter_lines(filepath)
195
+
196
+ # Check required fields
197
+ for field in ("name", "description", "tools", "model"):
198
+ if not _fm_has(fm_lines, field):
199
+ vr.error(f"{name} - Missing field: {field}")
200
+
201
+ # Check skill references exist
202
+ skills_val = _fm_field(fm_lines, "skills")
203
+ if skills_val:
204
+ for skill in (s.strip() for s in skills_val.split(",")):
205
+ if not skill:
206
+ continue
207
+ if not (tk_dir / "app" / "skills" / skill / "SKILL.md").is_file():
208
+ vr.warn(f"{name} - References non-existent skill: {skill}")
209
+
210
+ # Check tool names against whitelist
211
+ tools_val = _fm_field(fm_lines, "tools")
212
+ if tools_val:
213
+ for tool in (t.strip() for t in tools_val.split(",")):
214
+ if not tool:
215
+ continue
216
+ if tool not in VALID_TOOLS:
217
+ vr.warn(f"agent/{name}.md: unknown tool '{tool}' (not a standard Claude Code tool)")
218
+
219
+ print(f" Found: {agent_count} agents")
220
+ print()
221
+ return agent_count
222
+
223
+
224
+ def _validate_skill_frontmatter(tk_dir: Path, skill_path: Path,
225
+ fm_lines: list[str], vr: ValidationResult) -> None:
226
+ """Validate frontmatter fields for a single skill."""
227
+ name = skill_path.name
228
+ skill_file = skill_path / "SKILL.md"
229
+
230
+ if not _fm_has(fm_lines, "name"):
231
+ vr.error(f"{name} - Missing name field")
232
+ if not _fm_has(fm_lines, "description"):
233
+ vr.error(f"{name} - Missing description field")
234
+
235
+ name_value = _fm_field(fm_lines, "name").strip()
236
+ if name_value:
237
+ if len(name_value) > 64:
238
+ vr.error(f"{name} - Name exceeds 64 characters")
239
+ if re.search(r"[^a-z0-9-]", name_value):
240
+ vr.error(f"{name} - Name contains invalid characters (must be lowercase, numbers, hyphens)")
241
+
242
+ body_lines = _body_line_count(skill_file)
243
+ if body_lines > 500:
244
+ vr.warn(f"skills/{name}/SKILL.md: body is {body_lines} lines (recommended < 500)")
245
+
246
+ desc_value = _fm_field(fm_lines, "description")
247
+ if len(desc_value) > 1024:
248
+ vr.warn(f"{name} - Description exceeds 1024 characters")
249
+
250
+
251
+ def _validate_skill_references(tk_dir: Path, skill_path: Path,
252
+ fm_lines: list[str], vr: ValidationResult) -> None:
253
+ """Validate agent refs, depends-on, context/agent co-occurrence, and reference links."""
254
+ name = skill_path.name
255
+ skill_file = skill_path / "SKILL.md"
256
+
257
+ has_context_fork = _fm_field(fm_lines, "context").strip() == "fork"
258
+ has_agent = _fm_has(fm_lines, "agent")
259
+ if has_context_fork and not has_agent:
260
+ vr.warn(f"skills/{name}/SKILL.md: has 'context: fork' but missing 'agent:' field")
261
+ if has_agent and not has_context_fork:
262
+ vr.warn(f"skills/{name}/SKILL.md: has 'agent:' but missing 'context: fork'")
263
+
264
+ agent_value = _fm_field(fm_lines, "agent").strip()
265
+ if agent_value:
266
+ if not (tk_dir / "app" / "agents" / f"{agent_value}.md").is_file():
267
+ vr.error(f"skills/{name}/SKILL.md: agent '{agent_value}' not found in app/agents/")
268
+
269
+ depends_val = _fm_field(fm_lines, "depends-on")
270
+ if depends_val:
271
+ for dep in (d.strip() for d in depends_val.split(",")):
272
+ if not dep:
273
+ continue
274
+ if not (tk_dir / "app" / "skills" / dep / "SKILL.md").is_file():
275
+ vr.error(f"skills/{name}/SKILL.md: depends-on '{dep}' not found in app/skills/")
276
+
277
+ if _fm_has(fm_lines, "version"):
278
+ vr.warn(f"{name} - Uses deprecated 'version' field in frontmatter")
279
+ if _fm_has(fm_lines, "delegate-agent"):
280
+ vr.warn(f"{name} - Uses deprecated 'delegate-agent' field (rename to 'agent:')")
281
+ if _fm_has(fm_lines, "run-mode"):
282
+ vr.warn(f"{name} - Uses deprecated 'run-mode' field (rename to 'context:')")
283
+
284
+ ref_dir = skill_path / "reference"
285
+ if ref_dir.is_dir():
286
+ content = skill_file.read_text(encoding="utf-8")
287
+ for match in re.finditer(r"\(reference/([^)]+)\)", content):
288
+ ref_link = f"reference/{match.group(1)}"
289
+ if not (skill_path / ref_link).is_file():
290
+ vr.error(f"{name} - Broken reference link: {ref_link}")
291
+
292
+
293
+ def validate_skills(tk_dir: Path, vr: ValidationResult) -> int:
294
+ """Validate skill directories. Returns skill count."""
295
+ print("## Skills")
296
+ skills_dir = tk_dir / "app" / "skills"
297
+ skill_count = 0
298
+
299
+ if not skills_dir.is_dir():
300
+ vr.error("app/skills directory not found")
301
+ print()
302
+ return 0
303
+
304
+ for skill_path in sorted(skills_dir.iterdir()):
305
+ if not skill_path.is_dir() or skill_path.name.startswith("_"):
306
+ continue
307
+ skill_count += 1
308
+ name = skill_path.name
309
+ skill_file = skill_path / "SKILL.md"
310
+
311
+ if not skill_file.is_file():
312
+ vr.error(f"{name} - Missing SKILL.md")
313
+ continue
314
+
315
+ if not _has_frontmatter(skill_file):
316
+ vr.error(f"{name} - SKILL.md missing frontmatter")
317
+ continue
318
+
319
+ fm_lines = _parse_frontmatter_lines(skill_file)
320
+ _validate_skill_frontmatter(tk_dir, skill_path, fm_lines, vr)
321
+ _validate_skill_references(tk_dir, skill_path, fm_lines, vr)
322
+
323
+ print(f" Found: {skill_count} skills")
324
+ print()
325
+ return skill_count
326
+
327
+
328
+ def validate_legacy_commands(tk_dir: Path, vr: ValidationResult) -> None:
329
+ """Check for legacy command files."""
330
+ commands_dir = tk_dir / "app" / "commands"
331
+ if commands_dir.is_dir():
332
+ cmd_count = sum(1 for f in commands_dir.glob("*.md") if f.is_file())
333
+ if cmd_count > 0:
334
+ print("## Legacy Commands")
335
+ vr.warn(f"{cmd_count} command files found in app/commands/ (should be migrated to skills)")
336
+ print()
337
+
338
+
339
+ def validate_hook_events(tk_dir: Path, vr: ValidationResult) -> None:
340
+ """Validate hook event names in hooks.json."""
341
+ print("## Hook Events")
342
+ hooks_file = tk_dir / "app" / "hooks.json"
343
+
344
+ if not hooks_file.is_file():
345
+ vr.error("app/hooks.json not found")
346
+ print()
347
+ return
348
+
349
+ try:
350
+ with open(hooks_file, encoding="utf-8") as f:
351
+ data = json.load(f)
352
+ except (json.JSONDecodeError, OSError) as exc:
353
+ vr.error(f"Could not parse app/hooks.json: {exc}")
354
+ print()
355
+ return
356
+
357
+ hooks = data.get("hooks", {})
358
+ for event in hooks:
359
+ if event in VALID_HOOK_EVENTS:
360
+ print(f" OK: {event}")
361
+ else:
362
+ vr.error(f"Unknown hook event: {event}")
363
+
364
+ # Count hook scripts
365
+ hooks_dir = tk_dir / "app" / "hooks"
366
+ script_count = sum(1 for f in hooks_dir.glob("*.sh") if f.is_file()) if hooks_dir.is_dir() else 0
367
+ print(f" Found: {script_count} hook scripts")
368
+ print()
369
+
370
+
371
+ def validate_planned_assets(tk_dir: Path, vr: ValidationResult) -> None:
372
+ """Validate that planned assets exist and are non-empty."""
373
+ print("## Planned Assets")
374
+
375
+ for rel_path in PLANNED_ASSETS:
376
+ full = tk_dir / rel_path
377
+ if full.is_file() and full.stat().st_size > 0:
378
+ print(f" OK: {rel_path}")
379
+ else:
380
+ vr.error(f"Missing or empty {rel_path}")
381
+
382
+ # Validate plugin manifest JSON
383
+ plugin_manifest = tk_dir / "app" / ".claude-plugin" / "plugin.json"
384
+ if plugin_manifest.is_file():
385
+ try:
386
+ with open(plugin_manifest, encoding="utf-8") as f:
387
+ d = json.load(f)
388
+ assert d["name"]
389
+ assert d["version"]
390
+ assert d["description"]
391
+ print(" OK: plugin manifest JSON is valid")
392
+ except (json.JSONDecodeError, KeyError, AssertionError):
393
+ vr.error("Invalid plugin manifest JSON or missing required fields")
394
+
395
+ # Validate benchmark dashboard JSON
396
+ benchmark_dashboard = tk_dir / "benchmarks" / "ecosystem-dashboard.json"
397
+ if benchmark_dashboard.is_file():
398
+ try:
399
+ with open(benchmark_dashboard, encoding="utf-8") as f:
400
+ d = json.load(f)
401
+ assert d["generated_at"]
402
+ assert d["snapshot_date"]
403
+ assert d["freshness"]["status"]
404
+ assert isinstance(d["repos"], list) and len(d["repos"]) > 0
405
+ assert isinstance(d["comparison_matrix"], list) and len(d["comparison_matrix"]) > 0
406
+ print(" OK: benchmark dashboard JSON is valid")
407
+ except (json.JSONDecodeError, KeyError, AssertionError):
408
+ vr.error("Invalid benchmark dashboard JSON or missing required fields")
409
+
410
+ print()
411
+
412
+
413
+ def _validate_pack_manifest(manifest: Path, pack_name: str) -> dict | None:
414
+ """Parse and validate a plugin pack manifest. Returns data dict or None on failure."""
415
+ try:
416
+ with open(manifest, encoding="utf-8") as f:
417
+ d = json.load(f)
418
+ except (json.JSONDecodeError, OSError):
419
+ return None
420
+
421
+ errors = _validate_plugin_manifest_schema(d, manifest.parent)
422
+ if errors:
423
+ return None
424
+ return d
425
+
426
+
427
+ def _validate_pack_refs(tk_dir: Path, pack_path: Path, d: dict,
428
+ pack_name: str, vr: ValidationResult) -> None:
429
+ """Validate agent/skill references and hook shebangs for a plugin pack."""
430
+ ref_errors = _validate_plugin_references(
431
+ d,
432
+ agents_dir=tk_dir / "app" / "agents",
433
+ skills_dir=tk_dir / "app" / "skills",
434
+ )
435
+ for err in ref_errors:
436
+ vr.error(f"app/plugins/{pack_name}/plugin.json {err}")
437
+
438
+ hooks_dir = pack_path / "hooks"
439
+ if hooks_dir.is_dir():
440
+ for hook in sorted(hooks_dir.glob("*.sh")):
441
+ with open(hook, encoding="utf-8") as f:
442
+ first_line = f.readline().rstrip("\n")
443
+ rel = str(hook.relative_to(tk_dir))
444
+ if re.match(r"^#!/(usr/)?bin/(env )?bash", first_line):
445
+ print(f" OK: {rel}")
446
+ else:
447
+ vr.error(f"{rel} missing bash shebang")
448
+
449
+
450
+ def validate_plugin_packs(tk_dir: Path, vr: ValidationResult) -> None:
451
+ """Validate plugin pack manifests and references."""
452
+ print("## Plugin Packs")
453
+ plugin_dir = tk_dir / "app" / "plugins"
454
+
455
+ if not plugin_dir.is_dir():
456
+ vr.error("app/plugins directory not found")
457
+ print()
458
+ return
459
+
460
+ pack_count = 0
461
+ for pack_path in sorted(plugin_dir.iterdir()):
462
+ if not pack_path.is_dir():
463
+ continue
464
+ pack_name = pack_path.name
465
+ manifest = pack_path / "plugin.json"
466
+
467
+ if not manifest.is_file():
468
+ if pack_name == "plugins":
469
+ continue
470
+ vr.error(f"plugin pack {pack_name} missing plugin.json")
471
+ continue
472
+
473
+ d = _validate_pack_manifest(manifest, pack_name)
474
+ if d is None:
475
+ vr.error(f"Invalid plugin pack manifest: app/plugins/{pack_name}/plugin.json")
476
+ pack_count += 1
477
+ continue
478
+
479
+ print(f" OK: {pack_name}/plugin.json")
480
+ _validate_pack_refs(tk_dir, pack_path, d, pack_name, vr)
481
+ pack_count += 1
482
+
483
+ print(f" Found: {pack_count} plugin packs")
484
+ print()
485
+
486
+
487
+ def validate_kb_documents(tk_dir: Path, vr: ValidationResult) -> None:
488
+ """Validate KB document frontmatter."""
489
+ print("## KB Documents")
490
+ kb_dir = tk_dir / "kb"
491
+ kb_count = 0
492
+ kb_errors = 0
493
+
494
+ if not kb_dir.is_dir():
495
+ vr.error("kb/ directory not found")
496
+ print()
497
+ return
498
+
499
+ for kb_file in sorted(kb_dir.rglob("*.md")):
500
+ if kb_file.name == "README.md":
501
+ continue
502
+ kb_name = str(kb_file.relative_to(tk_dir))
503
+ kb_count += 1
504
+
505
+ if not _has_frontmatter(kb_file):
506
+ vr.error(f"{kb_name} - Missing YAML frontmatter")
507
+ kb_errors += 1
508
+ continue
509
+
510
+ fm_lines = _parse_frontmatter_lines(kb_file)
511
+
512
+ # Required fields
513
+ for field in ("title", "category", "service", "tags", "last_updated", "created", "description"):
514
+ if not _fm_has(fm_lines, field):
515
+ vr.error(f"{kb_name} - Missing required field: {field}")
516
+ kb_errors += 1
517
+
518
+ # Validate category
519
+ kb_category = _fm_field(fm_lines, "category").strip()
520
+ if kb_category and kb_category not in VALID_KB_CATEGORIES:
521
+ vr.error(f"{kb_name} - Invalid category '{kb_category}' (valid: {', '.join(sorted(VALID_KB_CATEGORIES))})")
522
+ kb_errors += 1
523
+
524
+ # Validate tags is not empty
525
+ tags_val = _fm_field(fm_lines, "tags")
526
+ if tags_val:
527
+ # Tags format: [tag1, tag2]
528
+ tag_items = re.findall(r"[a-z][\w-]*", tags_val)
529
+ if len(tag_items) < 1:
530
+ vr.warn(f"{kb_name} - Tags array is empty (minimum 1 tag recommended)")
531
+
532
+ if kb_errors == 0:
533
+ print(f" OK: {kb_count} KB documents validated")
534
+ else:
535
+ print(f" Found: {kb_count} KB documents ({kb_errors} with errors)")
536
+ print()
537
+
538
+
539
+ def validate_core_files(tk_dir: Path, vr: ValidationResult) -> None:
540
+ """Check that core files exist."""
541
+ print("## Core Files")
542
+ for rel in CORE_FILES:
543
+ if (tk_dir / rel).is_file():
544
+ print(f" OK: {rel}")
545
+ else:
546
+ vr.error(f"Missing {rel}")
547
+ print()
548
+
549
+
550
+ def _count_bats_tests(tk_dir: Path) -> str:
551
+ """Count @test entries in bats files. Returns count as string or empty."""
552
+ tests_dir = tk_dir / "tests"
553
+ if not tests_dir.is_dir():
554
+ return ""
555
+ total = 0
556
+ for bats_file in tests_dir.glob("*.bats"):
557
+ content = bats_file.read_text(encoding="utf-8")
558
+ total += len(re.findall(r"^@test ", content, re.MULTILINE))
559
+ return str(total) if total > 0 else ""
560
+
561
+
562
+ def _extract_readme_badges(tk_dir: Path) -> tuple[str, str, str]:
563
+ """Extract agent, skill, and test badge counts from README.md."""
564
+ readme = tk_dir / "README.md"
565
+ if not readme.is_file():
566
+ return "", "", ""
567
+ readme_content = readme.read_text(encoding="utf-8")
568
+ readme_agents = ""
569
+ readme_skills = ""
570
+ readme_tests = ""
571
+ m = re.search(r"agents-(\d+)", readme_content)
572
+ if m:
573
+ readme_agents = m.group(1)
574
+ m = re.search(r"skills-(\d+)", readme_content)
575
+ if m:
576
+ readme_skills = m.group(1)
577
+ m = re.search(r"tests-(\d+)", readme_content)
578
+ if m:
579
+ readme_tests = m.group(1)
580
+ return readme_agents, readme_skills, readme_tests
581
+
582
+
583
+ def validate_metadata_contracts(
584
+ tk_dir: Path,
585
+ agent_count: int,
586
+ skill_count: int,
587
+ vr: ValidationResult,
588
+ ) -> str:
589
+ """Validate README badge counts match actual counts. Returns actual_tests."""
590
+ print("## Metadata Contracts")
591
+
592
+ actual_tests = _count_bats_tests(tk_dir)
593
+ readme_agents, readme_skills, readme_tests = _extract_readme_badges(tk_dir)
594
+
595
+ if readme_agents and readme_agents != str(agent_count):
596
+ vr.error(f"README agent badge ({readme_agents}) != actual ({agent_count})")
597
+ else:
598
+ print(f" OK: agents ({agent_count})")
599
+
600
+ if readme_skills and readme_skills != str(skill_count):
601
+ vr.error(f"README skill badge ({readme_skills}) != actual ({skill_count})")
602
+ else:
603
+ print(f" OK: skills ({skill_count})")
604
+
605
+ if not actual_tests:
606
+ print(" SKIP: tests (tests/ not present in this installation)")
607
+ elif readme_tests and readme_tests != actual_tests:
608
+ vr.error(f"README test badge ({readme_tests}) != actual ({actual_tests})")
609
+ else:
610
+ print(f" OK: tests ({actual_tests})")
611
+
612
+ print()
613
+ return actual_tests
614
+
615
+
616
+ def validate_content_quality(tk_dir: Path, vr: ValidationResult) -> None:
617
+ """Check content quality: name matches directory, non-empty body."""
618
+ print()
619
+ print("## Content Quality")
620
+ skills_dir = tk_dir / "app" / "skills"
621
+
622
+ if not skills_dir.is_dir():
623
+ return
624
+
625
+ for skill_path in sorted(skills_dir.iterdir()):
626
+ if not skill_path.is_dir() or skill_path.name.startswith("_"):
627
+ continue
628
+ skill_file = skill_path / "SKILL.md"
629
+ if not skill_file.is_file():
630
+ continue
631
+ dir_name = skill_path.name
632
+
633
+ # Check name matches directory
634
+ file_name = frontmatter_field(skill_file, "name").strip()
635
+ if file_name and file_name != dir_name:
636
+ vr.warn(f"{dir_name} - name field '{file_name}' != directory name")
637
+
638
+ # Check non-empty body after frontmatter
639
+ body_nonblank = _body_nonblank_count(skill_file)
640
+ if body_nonblank == 0:
641
+ vr.error(f"{dir_name} - SKILL.md has no content after frontmatter")
642
+
643
+ print(" Done: content quality checks")
644
+
645
+
646
+ # ---------------------------------------------------------------------------
647
+ # Main
648
+ # ---------------------------------------------------------------------------
649
+
650
+ def _run_all_checks(tk_dir: Path, vr: ValidationResult) -> tuple[int, int, str]:
651
+ """Run all validation checks. Returns (agent_count, skill_count, actual_tests)."""
652
+ agent_count = validate_agents(tk_dir, vr)
653
+ skill_count = validate_skills(tk_dir, vr)
654
+ validate_legacy_commands(tk_dir, vr)
655
+ validate_hook_events(tk_dir, vr)
656
+ validate_planned_assets(tk_dir, vr)
657
+ validate_plugin_packs(tk_dir, vr)
658
+ validate_kb_documents(tk_dir, vr)
659
+ validate_core_files(tk_dir, vr)
660
+ actual_tests = validate_metadata_contracts(tk_dir, agent_count, skill_count, vr)
661
+ validate_content_quality(tk_dir, vr)
662
+ return agent_count, skill_count, actual_tests
663
+
664
+
665
+ def main() -> None:
666
+ strict = False
667
+ tk_dir = default_toolkit_dir
668
+
669
+ for arg in sys.argv[1:]:
670
+ if arg == "--strict":
671
+ strict = True
672
+ elif not arg.startswith("-"):
673
+ tk_dir = Path(arg)
674
+
675
+ print("AI Toolkit Validator")
676
+ print("========================")
677
+ print(f"Toolkit: {tk_dir}")
678
+ print()
679
+
680
+ vr = ValidationResult()
681
+ agent_count, skill_count, actual_tests = _run_all_checks(tk_dir, vr)
682
+
683
+ print("========================")
684
+ print(f"Summary: {agent_count} agents, {skill_count} skills, {actual_tests or 'n/a'} tests")
685
+ print(f"Errors: {vr.errors} | Warnings: {vr.warnings}")
686
+
687
+ if vr.errors > 0:
688
+ print("VALIDATION FAILED")
689
+ sys.exit(1)
690
+ else:
691
+ print("VALIDATION PASSED")
692
+ if strict and vr.warnings > 0:
693
+ print()
694
+ print(f"STRICT MODE: {vr.warnings} warning(s) treated as errors")
695
+ sys.exit(1)
696
+ sys.exit(0)
697
+
698
+
699
+ if __name__ == "__main__":
700
+ main()