@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.
- package/AGENTS.md +412 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +632 -0
- package/action.yml +53 -0
- package/app/.claude-plugin/plugin.json +44 -0
- package/app/ARCHITECTURE.md +306 -0
- package/app/CLAUDE.md.template +23 -0
- package/app/agents/ai-engineer.md +128 -0
- package/app/agents/backend-specialist.md +193 -0
- package/app/agents/business-intelligence.md +54 -0
- package/app/agents/chaos-monkey.md +67 -0
- package/app/agents/chief-of-staff.md +51 -0
- package/app/agents/code-archaeologist.md +127 -0
- package/app/agents/code-reviewer.md +184 -0
- package/app/agents/command-expert.md +131 -0
- package/app/agents/data-analyst.md +205 -0
- package/app/agents/data-scientist.md +151 -0
- package/app/agents/database-architect.md +317 -0
- package/app/agents/debugger.md +238 -0
- package/app/agents/devops-implementer.md +194 -0
- package/app/agents/documenter.md +364 -0
- package/app/agents/explorer-agent.md +145 -0
- package/app/agents/fact-checker.md +172 -0
- package/app/agents/frontend-specialist.md +209 -0
- package/app/agents/game-developer.md +216 -0
- package/app/agents/incident-responder.md +226 -0
- package/app/agents/infrastructure-architect.md +127 -0
- package/app/agents/infrastructure-validator.md +247 -0
- package/app/agents/llm-ops-engineer.md +237 -0
- package/app/agents/mcp-expert.md +228 -0
- package/app/agents/mcp-server-architect.md +195 -0
- package/app/agents/mcp-testing-engineer.md +292 -0
- package/app/agents/meta-architect.md +58 -0
- package/app/agents/ml-engineer.md +136 -0
- package/app/agents/mobile-developer.md +190 -0
- package/app/agents/night-watchman.md +55 -0
- package/app/agents/nlp-engineer.md +154 -0
- package/app/agents/orchestrator.md +437 -0
- package/app/agents/performance-optimizer.md +254 -0
- package/app/agents/predictive-analyst.md +57 -0
- package/app/agents/product-manager.md +194 -0
- package/app/agents/project-planner.md +287 -0
- package/app/agents/prompt-engineer.md +103 -0
- package/app/agents/qa-automation-engineer.md +182 -0
- package/app/agents/rag-engineer.md +201 -0
- package/app/agents/research-synthesizer.md +138 -0
- package/app/agents/search-specialist.md +101 -0
- package/app/agents/security-architect.md +62 -0
- package/app/agents/security-auditor.md +293 -0
- package/app/agents/seo-specialist.md +111 -0
- package/app/agents/system-governor.md +57 -0
- package/app/agents/tech-lead.md +62 -0
- package/app/agents/technical-researcher.md +103 -0
- package/app/agents/test-engineer.md +264 -0
- package/app/constitution.md +38 -0
- package/app/hooks/_profile-check.sh +11 -0
- package/app/hooks/guard-destructive.sh +74 -0
- package/app/hooks/guard-path.sh +73 -0
- package/app/hooks/post-tool-use.sh +35 -0
- package/app/hooks/pre-compact.sh +31 -0
- package/app/hooks/quality-check.sh +22 -0
- package/app/hooks/quality-gate.sh +49 -0
- package/app/hooks/save-session.sh +24 -0
- package/app/hooks/session-end.sh +37 -0
- package/app/hooks/session-start.sh +29 -0
- package/app/hooks/subagent-start.sh +16 -0
- package/app/hooks/subagent-stop.sh +16 -0
- package/app/hooks/track-usage.sh +50 -0
- package/app/hooks/user-prompt-submit.sh +25 -0
- package/app/hooks.json +178 -0
- package/app/mcp-defaults.json +23 -0
- package/app/output-styles/golden-rules.md +43 -0
- package/app/plugins/README.md +19 -0
- package/app/plugins/csharp-pack/README.md +11 -0
- package/app/plugins/csharp-pack/plugin.json +18 -0
- package/app/plugins/enterprise-pack/README.md +16 -0
- package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
- package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
- package/app/plugins/enterprise-pack/plugin.json +24 -0
- package/app/plugins/frontend-pack/README.md +14 -0
- package/app/plugins/frontend-pack/plugin.json +22 -0
- package/app/plugins/java-pack/README.md +11 -0
- package/app/plugins/java-pack/plugin.json +18 -0
- package/app/plugins/kotlin-pack/README.md +11 -0
- package/app/plugins/kotlin-pack/plugin.json +18 -0
- package/app/plugins/memory-pack/README.md +24 -0
- package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
- package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
- package/app/plugins/memory-pack/plugin.json +22 -0
- package/app/plugins/memory-pack/scripts/init_db.py +81 -0
- package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
- package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
- package/app/plugins/research-pack/README.md +14 -0
- package/app/plugins/research-pack/plugin.json +22 -0
- package/app/plugins/ruby-pack/README.md +11 -0
- package/app/plugins/ruby-pack/plugin.json +18 -0
- package/app/plugins/rust-pack/README.md +11 -0
- package/app/plugins/rust-pack/plugin.json +18 -0
- package/app/plugins/security-pack/README.md +15 -0
- package/app/plugins/security-pack/plugin.json +23 -0
- package/app/plugins/swift-pack/README.md +11 -0
- package/app/plugins/swift-pack/plugin.json +18 -0
- package/app/rules/claude-toolkit-rules.md +21 -0
- package/app/rules/git-conventions.md +5 -0
- package/app/rules/quality-gates.md +10 -0
- package/app/skills/_lib/__init__.py +1 -0
- package/app/skills/_lib/detect_utils.py +150 -0
- package/app/skills/agent-creator/SKILL.md +82 -0
- package/app/skills/analyze/SKILL.md +92 -0
- package/app/skills/analyze/scripts/complexity.py +165 -0
- package/app/skills/api-patterns/SKILL.md +305 -0
- package/app/skills/app-builder/SKILL.md +187 -0
- package/app/skills/architecture-audit/SKILL.md +141 -0
- package/app/skills/architecture-decision/SKILL.md +55 -0
- package/app/skills/architecture-decision/templates/adr-template.md +36 -0
- package/app/skills/biz-scan/SKILL.md +30 -0
- package/app/skills/briefing/SKILL.md +27 -0
- package/app/skills/build/SKILL.md +97 -0
- package/app/skills/build/scripts/detect-build.py +151 -0
- package/app/skills/chaos/SKILL.md +32 -0
- package/app/skills/ci/SKILL.md +77 -0
- package/app/skills/ci/scripts/ci-detect.py +135 -0
- package/app/skills/ci/templates/github-actions-node.yml +38 -0
- package/app/skills/ci/templates/github-actions-python.yml +42 -0
- package/app/skills/ci-cd-patterns/SKILL.md +299 -0
- package/app/skills/clean-code/SKILL.md +110 -0
- package/app/skills/clean-code/reference/dart.md +18 -0
- package/app/skills/clean-code/reference/go.md +23 -0
- package/app/skills/clean-code/reference/php.md +32 -0
- package/app/skills/clean-code/reference/python.md +180 -0
- package/app/skills/clean-code/reference/typescript.md +26 -0
- package/app/skills/command-creator/SKILL.md +83 -0
- package/app/skills/commit/SKILL.md +98 -0
- package/app/skills/commit/scripts/pre-commit-check.py +87 -0
- package/app/skills/commit/templates/conventional-commit.md +52 -0
- package/app/skills/csharp-patterns/SKILL.md +450 -0
- package/app/skills/database-patterns/SKILL.md +297 -0
- package/app/skills/debug/SKILL.md +154 -0
- package/app/skills/debug/scripts/error-parser.py +187 -0
- package/app/skills/debugging-tactics/SKILL.md +136 -0
- package/app/skills/deploy/SKILL.md +130 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
- package/app/skills/deploy/templates/deployment-checklist.md +31 -0
- package/app/skills/design-an-interface/SKILL.md +105 -0
- package/app/skills/design-engineering/SKILL.md +260 -0
- package/app/skills/docker-devops/SKILL.md +303 -0
- package/app/skills/docs/SKILL.md +145 -0
- package/app/skills/docs/scripts/doc-inventory.py +176 -0
- package/app/skills/docs/templates/adr-template.md +36 -0
- package/app/skills/docs/templates/readme-template.md +67 -0
- package/app/skills/documentation-standards/SKILL.md +191 -0
- package/app/skills/ecommerce-patterns/SKILL.md +209 -0
- package/app/skills/evaluate/SKILL.md +132 -0
- package/app/skills/evolve/SKILL.md +27 -0
- package/app/skills/explain/SKILL.md +54 -0
- package/app/skills/explain/scripts/dependency-graph.py +215 -0
- package/app/skills/explore/SKILL.md +112 -0
- package/app/skills/explore/scripts/visualize.py +117 -0
- package/app/skills/fix/SKILL.md +78 -0
- package/app/skills/fix/scripts/error-classifier.py +191 -0
- package/app/skills/flutter-patterns/SKILL.md +254 -0
- package/app/skills/git-mastery/SKILL.md +70 -0
- package/app/skills/grill-me/SKILL.md +38 -0
- package/app/skills/health/SKILL.md +91 -0
- package/app/skills/health/scripts/health_check.py +162 -0
- package/app/skills/hive-mind/SKILL.md +56 -0
- package/app/skills/hook-creator/SKILL.md +107 -0
- package/app/skills/index/SKILL.md +74 -0
- package/app/skills/instinct-review/SKILL.md +77 -0
- package/app/skills/java-patterns/SKILL.md +442 -0
- package/app/skills/kotlin-patterns/SKILL.md +446 -0
- package/app/skills/lint/SKILL.md +103 -0
- package/app/skills/lint/scripts/detect-linters.py +112 -0
- package/app/skills/mcp-patterns/SKILL.md +270 -0
- package/app/skills/mem-search/SKILL.md +70 -0
- package/app/skills/migrate/SKILL.md +90 -0
- package/app/skills/migrate/scripts/migration-status.py +195 -0
- package/app/skills/migration-patterns/SKILL.md +260 -0
- package/app/skills/night-watch/SKILL.md +28 -0
- package/app/skills/observability-patterns/SKILL.md +203 -0
- package/app/skills/onboard/SKILL.md +76 -0
- package/app/skills/orchestrate/SKILL.md +86 -0
- package/app/skills/panic/SKILL.md +30 -0
- package/app/skills/performance-profiling/SKILL.md +59 -0
- package/app/skills/plan/SKILL.md +110 -0
- package/app/skills/plan/templates/plan-template.md +40 -0
- package/app/skills/plan-writing/SKILL.md +201 -0
- package/app/skills/plugin-creator/SKILL.md +78 -0
- package/app/skills/pr/SKILL.md +129 -0
- package/app/skills/pr/scripts/pr-summary.py +175 -0
- package/app/skills/prd-to-issues/SKILL.md +108 -0
- package/app/skills/prd-to-plan/SKILL.md +120 -0
- package/app/skills/predict/SKILL.md +30 -0
- package/app/skills/qa-session/SKILL.md +110 -0
- package/app/skills/rag-patterns/SKILL.md +203 -0
- package/app/skills/refactor/SKILL.md +124 -0
- package/app/skills/refactor/scripts/refactor-scan.py +210 -0
- package/app/skills/refactor-plan/SKILL.md +112 -0
- package/app/skills/repeat/SKILL.md +149 -0
- package/app/skills/research-mastery/SKILL.md +56 -0
- package/app/skills/review/SKILL.md +141 -0
- package/app/skills/review/scripts/diff-analyzer.py +170 -0
- package/app/skills/rollback/SKILL.md +87 -0
- package/app/skills/rollback/scripts/rollback_info.py +149 -0
- package/app/skills/ruby-patterns/SKILL.md +454 -0
- package/app/skills/rust-patterns/SKILL.md +446 -0
- package/app/skills/search/SKILL.md +64 -0
- package/app/skills/security-patterns/SKILL.md +91 -0
- package/app/skills/security-patterns/reference/authentication.md +37 -0
- package/app/skills/security-patterns/reference/authorization.md +22 -0
- package/app/skills/security-patterns/reference/input-validation.md +30 -0
- package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
- package/app/skills/skill-creator/SKILL.md +154 -0
- package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
- package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
- package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
- package/app/skills/subagent-development/SKILL.md +225 -0
- package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
- package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
- package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
- package/app/skills/swarm/SKILL.md +81 -0
- package/app/skills/swift-patterns/SKILL.md +500 -0
- package/app/skills/tdd/SKILL.md +174 -0
- package/app/skills/tdd/reference/deep-modules.md +32 -0
- package/app/skills/tdd/reference/interface-design.md +32 -0
- package/app/skills/tdd/reference/mocking.md +52 -0
- package/app/skills/tdd/reference/refactoring.md +10 -0
- package/app/skills/tdd/reference/tests.md +59 -0
- package/app/skills/teams/SKILL.md +101 -0
- package/app/skills/test/SKILL.md +107 -0
- package/app/skills/test/scripts/detect-runner.py +113 -0
- package/app/skills/testing-patterns/SKILL.md +73 -0
- package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
- package/app/skills/testing-patterns/reference/go-testing.md +52 -0
- package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
- package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
- package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
- package/app/skills/triage-issue/SKILL.md +120 -0
- package/app/skills/typescript-patterns/SKILL.md +256 -0
- package/app/skills/ubiquitous-language/SKILL.md +74 -0
- package/app/skills/verification-before-completion/SKILL.md +108 -0
- package/app/skills/workflow/SKILL.md +250 -0
- package/app/skills/write-a-prd/SKILL.md +129 -0
- package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
- package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
- package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
- package/app/templates/skill/generator/SKILL.md.template +40 -0
- package/app/templates/skill/knowledge/SKILL.md.template +52 -0
- package/app/templates/skill/linter/SKILL.md.template +34 -0
- package/app/templates/skill/reviewer/SKILL.md.template +51 -0
- package/app/templates/skill/workflow/SKILL.md.template +49 -0
- package/benchmarks/README.md +111 -0
- package/benchmarks/ecosystem-dashboard.json +148 -0
- package/benchmarks/ecosystem-harvest.json +148 -0
- package/benchmarks/results.json +38 -0
- package/benchmarks/run.py +351 -0
- package/bin/ai-toolkit.js +345 -0
- package/kb/best-practices/README.md +11 -0
- package/kb/howto/README.md +11 -0
- package/kb/procedures/maintenance-sop.md +306 -0
- package/kb/reference/agents-catalog.md +124 -0
- package/kb/reference/anti-pattern-registry-format.md +221 -0
- package/kb/reference/architecture-overview.md +232 -0
- package/kb/reference/benchmark-config.md +62 -0
- package/kb/reference/ci-integration.md +66 -0
- package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
- package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
- package/kb/reference/commands-catalog.md +21 -0
- package/kb/reference/distribution-model.md +63 -0
- package/kb/reference/global-install-model.md +56 -0
- package/kb/reference/hierarchical-override-pattern.md +200 -0
- package/kb/reference/hooks-catalog.md +306 -0
- package/kb/reference/integrations.md +88 -0
- package/kb/reference/language-packs.md +52 -0
- package/kb/reference/merge-friendly-install-model.md +58 -0
- package/kb/reference/plugin-pack-conventions.md +151 -0
- package/kb/reference/quick-wins-implementation-summary.md +70 -0
- package/kb/reference/skill-templates.md +50 -0
- package/kb/reference/skills-catalog.md +215 -0
- package/kb/reference/skills-unification.md +57 -0
- package/kb/reference/stats.md +69 -0
- package/kb/reference/sync.md +76 -0
- package/kb/troubleshooting/README.md +11 -0
- package/llms-full.txt +3068 -0
- package/llms.txt +39 -0
- package/package.json +75 -0
- package/scripts/_common.py +160 -0
- package/scripts/add_rule.py +50 -0
- package/scripts/benchmark_config.py +127 -0
- package/scripts/benchmark_ecosystem.py +288 -0
- package/scripts/check_deps.py +260 -0
- package/scripts/create_skill.py +118 -0
- package/scripts/doctor.py +504 -0
- package/scripts/eject.py +113 -0
- package/scripts/emission.py +256 -0
- package/scripts/evaluate_skills.py +260 -0
- package/scripts/frontmatter.py +58 -0
- package/scripts/generate_agents_md.py +91 -0
- package/scripts/generate_aider_conf.py +51 -0
- package/scripts/generate_cline.py +35 -0
- package/scripts/generate_copilot.py +30 -0
- package/scripts/generate_cursor_rules.py +35 -0
- package/scripts/generate_gemini.py +28 -0
- package/scripts/generate_llms_txt.py +164 -0
- package/scripts/generate_roo_modes.py +80 -0
- package/scripts/generate_windsurf.py +35 -0
- package/scripts/generator_base.py +140 -0
- package/scripts/harvest_ecosystem.py +50 -0
- package/scripts/inject_rule_cli.py +101 -0
- package/scripts/inject_section_cli.py +47 -0
- package/scripts/injection.py +180 -0
- package/scripts/install.py +236 -0
- package/scripts/install_git_hooks.py +71 -0
- package/scripts/install_steps/__init__.py +5 -0
- package/scripts/install_steps/ai_tools.py +261 -0
- package/scripts/install_steps/hooks.py +90 -0
- package/scripts/install_steps/markers.py +79 -0
- package/scripts/install_steps/symlinks.py +87 -0
- package/scripts/merge-hooks.py +192 -0
- package/scripts/plugin.py +642 -0
- package/scripts/plugin_schema.py +138 -0
- package/scripts/remove_rule.py +58 -0
- package/scripts/stats.py +81 -0
- package/scripts/sync.py +215 -0
- package/scripts/uninstall.py +292 -0
- 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()
|