@cleocode/cleo 2026.3.20 → 2026.3.23
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/dist/cli/index.js +46156 -42192
- package/dist/cli/index.js.map +4 -4
- package/dist/mcp/index.js +37601 -36260
- package/dist/mcp/index.js.map +4 -4
- package/drizzle-brain.config.ts +7 -0
- package/drizzle-nexus.config.ts +7 -0
- package/drizzle-tasks.config.ts +7 -0
- package/migrations/drizzle-brain/20260301230215_workable_spitfire/migration.sql +68 -0
- package/migrations/drizzle-brain/20260301230215_workable_spitfire/snapshot.json +651 -0
- package/migrations/drizzle-brain/20260302050325_unknown_justin_hammer/migration.sql +23 -0
- package/migrations/drizzle-brain/20260302050325_unknown_justin_hammer/snapshot.json +884 -0
- package/migrations/drizzle-brain/20260302061755_unusual_jamie_braddock/migration.sql +2 -0
- package/migrations/drizzle-brain/20260302061755_unusual_jamie_braddock/snapshot.json +908 -0
- package/migrations/drizzle-brain/20260302193548_luxuriant_glorian/migration.sql +20 -0
- package/migrations/drizzle-brain/20260302193548_luxuriant_glorian/snapshot.json +1078 -0
- package/migrations/drizzle-brain/20260304045002_white_thunderbolt_ross/migration.sql +16 -0
- package/migrations/drizzle-brain/20260304045002_white_thunderbolt_ross/snapshot.json +1233 -0
- package/migrations/drizzle-nexus/20260305070805_quick_ted_forrester/migration.sql +46 -0
- package/migrations/drizzle-nexus/20260305070805_quick_ted_forrester/snapshot.json +461 -0
- package/migrations/drizzle-tasks/20260308024513_oval_king_bedlam/migration.sql +32 -0
- package/migrations/drizzle-tasks/20260308024513_oval_king_bedlam/snapshot.json +3727 -0
- package/package.json +22 -5
- package/packages/ct-skills/skills/ct-cleo/SKILL.md +344 -81
- package/packages/ct-skills/skills/ct-grade/SKILL.md +20 -4
- package/packages/ct-skills/skills/ct-grade/agents/analysis-reporter.md +203 -0
- package/packages/ct-skills/skills/ct-grade/agents/blind-comparator.md +157 -0
- package/packages/ct-skills/skills/ct-grade/agents/scenario-runner.md +134 -0
- package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_grade_review.py +1138 -0
- package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_grade_viewer.py +544 -0
- package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_review.py +283 -0
- package/packages/ct-skills/skills/ct-grade/eval-viewer/grade-review.html +1574 -0
- package/packages/ct-skills/skills/ct-grade/eval-viewer/viewer.html +219 -0
- package/packages/ct-skills/skills/ct-grade/evals/evals.json +94 -0
- package/packages/ct-skills/skills/ct-grade/references/ab-test-methodology.md +150 -0
- package/packages/ct-skills/skills/ct-grade/references/domains.md +137 -0
- package/packages/ct-skills/skills/ct-grade/references/grade-spec.md +236 -0
- package/packages/ct-skills/skills/ct-grade/references/scenario-playbook.md +234 -0
- package/packages/ct-skills/skills/ct-grade/references/token-tracking.md +120 -0
- package/packages/ct-skills/skills/ct-grade/scripts/audit_analyzer.py +279 -0
- package/packages/ct-skills/skills/ct-grade/scripts/generate_report.py +283 -0
- package/packages/ct-skills/skills/ct-grade/scripts/run_ab_test.py +504 -0
- package/packages/ct-skills/skills/ct-grade/scripts/run_all.py +287 -0
- package/packages/ct-skills/skills/ct-grade/scripts/setup_run.py +183 -0
- package/packages/ct-skills/skills/ct-grade/scripts/token_tracker.py +630 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/SKILL.md +237 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/agents/analysis-reporter.md +203 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/agents/blind-comparator.md +157 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/agents/scenario-runner.md +179 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/evals/evals.json +74 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/build_op_stats.py +174 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/eval-analysis.json +41 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/eval-report.md +34 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/generate_grade_review.py +1023 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/generate_grade_viewer.py +548 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/grade-review-eval.html +613 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/grade-review.html +1532 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/viewer.html +620 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/manifest-entry.json +31 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/references/ab-testing.md +233 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/references/domains-ssot.md +156 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/references/grade-spec-v2.md +167 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/references/playbook-v2.md +393 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/references/token-tracking.md +202 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/scripts/generate_report.py +419 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/scripts/run_ab_test.py +493 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/scripts/run_scenario.py +396 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/scripts/setup_run.py +207 -0
- package/packages/ct-skills/skills/ct-grade-v2-1/scripts/token_tracker.py +175 -0
- package/packages/ct-skills/skills/ct-orchestrator/SKILL.md +1 -29
- package/packages/ct-skills/skills/ct-orchestrator/manifest-entry.json +19 -0
- package/packages/ct-skills/skills/ct-skill-creator/.cleo/.context-state.json +13 -0
- package/packages/ct-skills/skills/ct-skill-creator/.cleo/tasks.db +0 -0
- package/packages/ct-skills/skills/ct-skill-creator/SKILL.md +0 -12
- package/packages/ct-skills/skills/ct-skill-creator/agents/analyzer.md +276 -0
- package/packages/ct-skills/skills/ct-skill-creator/agents/comparator.md +204 -0
- package/packages/ct-skills/skills/ct-skill-creator/agents/grader.md +225 -0
- package/packages/ct-skills/skills/ct-skill-creator/assets/eval_review.html +146 -0
- package/packages/ct-skills/skills/ct-skill-creator/eval-viewer/generate_review.py +471 -0
- package/packages/ct-skills/skills/ct-skill-creator/eval-viewer/viewer.html +1325 -0
- package/packages/ct-skills/skills/ct-skill-creator/manifest-entry.json +17 -0
- package/packages/ct-skills/skills/ct-skill-creator/references/dynamic-context.md +228 -0
- package/packages/ct-skills/skills/ct-skill-creator/references/frontmatter.md +83 -0
- package/packages/ct-skills/skills/ct-skill-creator/references/invocation-control.md +165 -0
- package/packages/ct-skills/skills/ct-skill-creator/references/provider-deployment.md +175 -0
- package/packages/ct-skills/skills/ct-skill-creator/references/schemas.md +430 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/__init__.py +1 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/generate_report.py +326 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/improve_description.py +247 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/run_eval.py +310 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/run_loop.py +328 -0
- package/packages/ct-skills/skills/ct-skill-creator/scripts/utils.py +47 -0
- package/packages/ct-skills/skills/ct-skill-validator/SKILL.md +178 -0
- package/packages/ct-skills/skills/ct-skill-validator/agents/ecosystem-checker.md +151 -0
- package/packages/ct-skills/skills/ct-skill-validator/assets/valid-skill-example.md +13 -0
- package/packages/ct-skills/skills/ct-skill-validator/evals/eval_set.json +14 -0
- package/packages/ct-skills/skills/ct-skill-validator/evals/evals.json +52 -0
- package/packages/ct-skills/skills/ct-skill-validator/manifest-entry.json +20 -0
- package/packages/ct-skills/skills/ct-skill-validator/references/cleo-ecosystem-rules.md +163 -0
- package/packages/ct-skills/skills/ct-skill-validator/references/validation-rules.md +168 -0
- package/packages/ct-skills/skills/ct-skill-validator/scripts/__init__.py +0 -0
- package/packages/ct-skills/skills/ct-skill-validator/scripts/audit_body.py +242 -0
- package/packages/ct-skills/skills/ct-skill-validator/scripts/check_ecosystem.py +169 -0
- package/packages/ct-skills/skills/ct-skill-validator/scripts/check_manifest.py +172 -0
- package/packages/ct-skills/skills/ct-skill-validator/scripts/generate_validation_report.py +442 -0
- package/packages/ct-skills/skills/ct-skill-validator/scripts/validate.py +422 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260224040019_baseline/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260224040019_baseline/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260224040238_add-audit-log/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260224040238_add-audit-log/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260225024442_sync-lifecycle-enums-and-arch-decisions/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260225024442_sync-lifecycle-enums-and-arch-decisions/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227014821_adr-system-and-status-registry/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227014821_adr-system-and-status-registry/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227021231_add-cancelled-pipeline-status/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227021231_add-cancelled-pipeline-status/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227022417_adr-cognitive-search-fields/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227022417_adr-cognitive-search-fields/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227183521_parent-id-on-delete-set-null/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227183521_parent-id-on-delete-set-null/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227235745_add-audit-log-dispatch-columns/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260227235745_add-audit-log-dispatch-columns/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260301180528_update-task-relations-check-constraint/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260301180528_update-task-relations-check-constraint/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260302163443_free_silk_fever/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260302163443_free_silk_fever/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260302163511_late_sphinx/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260302163511_late_sphinx/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260305011924_cheerful_mongu/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260305011924_cheerful_mongu/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260305203927_demonic_storm/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260305203927_demonic_storm/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260306001243_spooky_rage/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260306001243_spooky_rage/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260306193138_young_morbius/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260306193138_young_morbius/snapshot.json +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/migration.sql +0 -0
- /package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/snapshot.json +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLEO Skill Validator — Full compliance gauntlet.
|
|
4
|
+
Validates a skill directory against the complete CLEO skill standard.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
validate.py <skill-directory>
|
|
8
|
+
validate.py <skill-directory> --manifest path/to/manifest.json
|
|
9
|
+
validate.py <skill-directory> --manifest path/to/manifest.json --dispatch-config path/to/dispatch-config.json
|
|
10
|
+
validate.py <skill-directory> --provider-map path/to/provider-skills-map.json
|
|
11
|
+
validate.py <skill-directory> --json
|
|
12
|
+
"""
|
|
13
|
+
import sys
|
|
14
|
+
import re
|
|
15
|
+
import json
|
|
16
|
+
import yaml
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
V2_STANDARD = {
|
|
21
|
+
"name", "description", "argument-hint", "disable-model-invocation",
|
|
22
|
+
"user-invocable", "allowed-tools", "model", "context", "agent", "hooks",
|
|
23
|
+
"license",
|
|
24
|
+
}
|
|
25
|
+
CLEO_ONLY = {
|
|
26
|
+
"version", "tier", "core", "category", "protocol",
|
|
27
|
+
"dependencies", "sharedResources", "compatibility",
|
|
28
|
+
"token_budget", "capabilities", "constraints",
|
|
29
|
+
"metadata", "tags", "triggers", "mvi_scope", "requires_tiers",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
MANIFEST_REQUIRED_FIELDS = [
|
|
33
|
+
"name", "version", "description", "path", "status",
|
|
34
|
+
"tier", "token_budget", "capabilities", "constraints",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_skill(skill_path, manifest_path=None, dispatch_config_path=None, provider_map_path=None):
|
|
39
|
+
"""Run the full v2 validation gauntlet on a skill directory.
|
|
40
|
+
|
|
41
|
+
Returns (results, errors, warnings) where results is a list of
|
|
42
|
+
(tier, severity, message) tuples.
|
|
43
|
+
"""
|
|
44
|
+
skill_dir = Path(skill_path).resolve()
|
|
45
|
+
skill_name = skill_dir.name
|
|
46
|
+
errors = 0
|
|
47
|
+
warnings = 0
|
|
48
|
+
results = []
|
|
49
|
+
|
|
50
|
+
def error(tier, msg):
|
|
51
|
+
nonlocal errors
|
|
52
|
+
errors += 1
|
|
53
|
+
results.append((tier, "ERROR", msg))
|
|
54
|
+
|
|
55
|
+
def warn(tier, msg):
|
|
56
|
+
nonlocal warnings
|
|
57
|
+
warnings += 1
|
|
58
|
+
results.append((tier, "WARN", msg))
|
|
59
|
+
|
|
60
|
+
def ok(tier, msg):
|
|
61
|
+
results.append((tier, "OK", msg))
|
|
62
|
+
|
|
63
|
+
# ── Tier 1 — Structure ──────────────────────────────────────────────
|
|
64
|
+
tier = 1
|
|
65
|
+
skill_md = skill_dir / "SKILL.md"
|
|
66
|
+
|
|
67
|
+
if not skill_md.exists():
|
|
68
|
+
error(tier, "SKILL.md does not exist")
|
|
69
|
+
return results, errors, warnings
|
|
70
|
+
|
|
71
|
+
ok(tier, "SKILL.md exists")
|
|
72
|
+
|
|
73
|
+
raw_content = skill_md.read_text(encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
if not raw_content.startswith("---"):
|
|
76
|
+
error(tier, "SKILL.md does not start with '---' (no frontmatter block)")
|
|
77
|
+
return results, errors, warnings
|
|
78
|
+
|
|
79
|
+
ok(tier, "Content starts with '---'")
|
|
80
|
+
|
|
81
|
+
fm_match = re.match(r"^---\n(.*?)\n---", raw_content, re.DOTALL)
|
|
82
|
+
if not fm_match:
|
|
83
|
+
error(tier, "Could not extract frontmatter (missing closing '---')")
|
|
84
|
+
return results, errors, warnings
|
|
85
|
+
|
|
86
|
+
ok(tier, "Frontmatter block extracted")
|
|
87
|
+
|
|
88
|
+
raw_frontmatter = fm_match.group(1)
|
|
89
|
+
try:
|
|
90
|
+
frontmatter = yaml.safe_load(raw_frontmatter)
|
|
91
|
+
except yaml.YAMLError as e:
|
|
92
|
+
error(tier, f"Frontmatter is not valid YAML: {e}")
|
|
93
|
+
return results, errors, warnings
|
|
94
|
+
|
|
95
|
+
ok(tier, "Frontmatter is valid YAML")
|
|
96
|
+
|
|
97
|
+
if not isinstance(frontmatter, dict):
|
|
98
|
+
error(tier, "Frontmatter is not a dictionary (key: value pairs expected)")
|
|
99
|
+
return results, errors, warnings
|
|
100
|
+
|
|
101
|
+
ok(tier, "Frontmatter is a dict")
|
|
102
|
+
|
|
103
|
+
for key in frontmatter:
|
|
104
|
+
if key in CLEO_ONLY:
|
|
105
|
+
error(tier, f"Move '{key}' to manifest.json (CLEO-only field)")
|
|
106
|
+
else:
|
|
107
|
+
pass # valid or unknown keys checked in tier 2
|
|
108
|
+
|
|
109
|
+
if not any(r[1] == "ERROR" and "CLEO-only" in r[2] for r in results):
|
|
110
|
+
ok(tier, "No CLEO-only fields in frontmatter")
|
|
111
|
+
|
|
112
|
+
# ── Tier 2 — Frontmatter Quality ────────────────────────────────────
|
|
113
|
+
tier = 2
|
|
114
|
+
|
|
115
|
+
# name checks
|
|
116
|
+
name_val = frontmatter.get("name")
|
|
117
|
+
if name_val is None:
|
|
118
|
+
error(tier, "'name' field is missing")
|
|
119
|
+
else:
|
|
120
|
+
if not isinstance(name_val, str):
|
|
121
|
+
error(tier, "'name' must be a string")
|
|
122
|
+
else:
|
|
123
|
+
if not re.match(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", name_val):
|
|
124
|
+
error(tier, f"'name' must be hyphen-case (got: '{name_val}')")
|
|
125
|
+
if "--" in name_val:
|
|
126
|
+
error(tier, "'name' must not contain consecutive hyphens")
|
|
127
|
+
if name_val.startswith("-") or name_val.endswith("-"):
|
|
128
|
+
error(tier, "'name' must not start or end with a hyphen")
|
|
129
|
+
if len(name_val) > 64:
|
|
130
|
+
error(tier, f"'name' exceeds 64 characters (got: {len(name_val)})")
|
|
131
|
+
if name_val != skill_name:
|
|
132
|
+
warn(tier, f"'name' field ('{name_val}') does not match directory name ('{skill_name}')")
|
|
133
|
+
if not any(r[1] == "ERROR" and "'name'" in r[2] for r in results if r[0] == 2):
|
|
134
|
+
ok(tier, "'name' is valid")
|
|
135
|
+
|
|
136
|
+
# description checks
|
|
137
|
+
desc_val = frontmatter.get("description")
|
|
138
|
+
if desc_val is None:
|
|
139
|
+
error(tier, "'description' field is missing")
|
|
140
|
+
else:
|
|
141
|
+
if not isinstance(desc_val, str):
|
|
142
|
+
error(tier, "'description' must be a string")
|
|
143
|
+
else:
|
|
144
|
+
if "<" in desc_val or ">" in desc_val:
|
|
145
|
+
error(tier, "'description' must not contain '<' or '>' characters")
|
|
146
|
+
if len(desc_val) > 1024:
|
|
147
|
+
error(tier, f"'description' exceeds 1024 characters (got: {len(desc_val)})")
|
|
148
|
+
if len(desc_val) < 50:
|
|
149
|
+
warn(tier, f"'description' is shorter than 50 characters (got: {len(desc_val)})")
|
|
150
|
+
trigger_indicators = ["when", "use when", "use for"]
|
|
151
|
+
has_trigger = any(ind in desc_val.lower() for ind in trigger_indicators)
|
|
152
|
+
if not has_trigger:
|
|
153
|
+
warn(tier, "'description' should contain a trigger indicator ('when', 'use when', 'use for')")
|
|
154
|
+
if desc_val.startswith("I "):
|
|
155
|
+
warn(tier, "'description' should not start with 'I ' (use third person)")
|
|
156
|
+
if not any(r[1] == "ERROR" and "'description'" in r[2] for r in results if r[0] == 2):
|
|
157
|
+
ok(tier, "'description' is valid")
|
|
158
|
+
|
|
159
|
+
# YAML multiline pitfall check
|
|
160
|
+
if "description: >" in raw_frontmatter or "description: |" in raw_frontmatter:
|
|
161
|
+
warn(tier, "'description' uses YAML multiline syntax (> or |) which can cause unexpected whitespace")
|
|
162
|
+
|
|
163
|
+
# context checks
|
|
164
|
+
context_val = frontmatter.get("context")
|
|
165
|
+
if context_val is not None:
|
|
166
|
+
if context_val != "fork":
|
|
167
|
+
error(tier, f"'context' must be 'fork' if present (got: '{context_val}')")
|
|
168
|
+
else:
|
|
169
|
+
if "agent" not in frontmatter:
|
|
170
|
+
warn(tier, "'context' is 'fork' but no 'agent' field specified")
|
|
171
|
+
ok(tier, "'context' is valid")
|
|
172
|
+
|
|
173
|
+
# boolean field checks
|
|
174
|
+
dmi_val = frontmatter.get("disable-model-invocation")
|
|
175
|
+
if dmi_val is not None and not isinstance(dmi_val, bool):
|
|
176
|
+
error(tier, "'disable-model-invocation' must be a boolean")
|
|
177
|
+
|
|
178
|
+
ui_val = frontmatter.get("user-invocable")
|
|
179
|
+
if ui_val is not None and not isinstance(ui_val, bool):
|
|
180
|
+
error(tier, "'user-invocable' must be a boolean")
|
|
181
|
+
|
|
182
|
+
# contradictory flags
|
|
183
|
+
if (isinstance(dmi_val, bool) and dmi_val is True and
|
|
184
|
+
isinstance(ui_val, bool) and ui_val is False):
|
|
185
|
+
error(tier, "Contradictory: 'disable-model-invocation' is true AND 'user-invocable' is false (skill cannot be invoked at all)")
|
|
186
|
+
|
|
187
|
+
# argument-hint checks
|
|
188
|
+
ah_val = frontmatter.get("argument-hint")
|
|
189
|
+
if ah_val is not None:
|
|
190
|
+
if not isinstance(ah_val, str):
|
|
191
|
+
error(tier, "'argument-hint' must be a string")
|
|
192
|
+
elif len(ah_val) > 100:
|
|
193
|
+
error(tier, f"'argument-hint' exceeds 100 characters (got: {len(ah_val)})")
|
|
194
|
+
|
|
195
|
+
# allowed-tools checks
|
|
196
|
+
at_val = frontmatter.get("allowed-tools")
|
|
197
|
+
if at_val is not None:
|
|
198
|
+
if not isinstance(at_val, (str, list)):
|
|
199
|
+
error(tier, "'allowed-tools' must be a string or list")
|
|
200
|
+
|
|
201
|
+
# model checks
|
|
202
|
+
model_val = frontmatter.get("model")
|
|
203
|
+
if model_val is not None and not isinstance(model_val, str):
|
|
204
|
+
error(tier, "'model' must be a string")
|
|
205
|
+
|
|
206
|
+
# agent checks
|
|
207
|
+
agent_val = frontmatter.get("agent")
|
|
208
|
+
if agent_val is not None and not isinstance(agent_val, str):
|
|
209
|
+
error(tier, "'agent' must be a string")
|
|
210
|
+
|
|
211
|
+
# hooks checks
|
|
212
|
+
hooks_val = frontmatter.get("hooks")
|
|
213
|
+
if hooks_val is not None and not isinstance(hooks_val, dict):
|
|
214
|
+
error(tier, "'hooks' must be a dict")
|
|
215
|
+
|
|
216
|
+
# ── Tier 3 — Body Quality ───────────────────────────────────────────
|
|
217
|
+
tier = 3
|
|
218
|
+
|
|
219
|
+
# Extract body (content after second ---)
|
|
220
|
+
parts = raw_content.split("---", 2)
|
|
221
|
+
body = parts[2].strip() if len(parts) >= 3 else ""
|
|
222
|
+
|
|
223
|
+
if not body:
|
|
224
|
+
warn(tier, "Body is empty (no content after frontmatter)")
|
|
225
|
+
else:
|
|
226
|
+
ok(tier, "Body is present")
|
|
227
|
+
|
|
228
|
+
body_lines = body.split("\n")
|
|
229
|
+
line_count = len(body_lines)
|
|
230
|
+
|
|
231
|
+
if line_count >= 600:
|
|
232
|
+
error(tier, f"Body is too long: {line_count} lines (max 600)")
|
|
233
|
+
elif line_count >= 400:
|
|
234
|
+
warn(tier, f"Body is getting long: {line_count} lines (warn threshold: 400)")
|
|
235
|
+
else:
|
|
236
|
+
ok(tier, f"Body length OK ({line_count} lines)")
|
|
237
|
+
|
|
238
|
+
# Placeholder scan — case-sensitive to avoid matching "todo app", "replace with X", etc.
|
|
239
|
+
placeholders = [r"\[Required:", r"\bTODO\b", r"\bREPLACE\b", r"\[Add content", r"\bFIXME\b", r"\bTBD\b"]
|
|
240
|
+
for pattern in placeholders:
|
|
241
|
+
matches = re.findall(pattern, body)
|
|
242
|
+
if matches:
|
|
243
|
+
clean_pattern = re.sub(r"[\\(?\[\])]", "", pattern).strip("\\b")
|
|
244
|
+
warn(tier, f"Placeholder text found: '{clean_pattern}' ({len(matches)} occurrence(s))")
|
|
245
|
+
|
|
246
|
+
# Section headers check for long bodies
|
|
247
|
+
if line_count > 200:
|
|
248
|
+
section_headers = re.findall(r"^## ", body, re.MULTILINE)
|
|
249
|
+
if not section_headers:
|
|
250
|
+
warn(tier, "Body exceeds 200 lines but has no '## ' section headers")
|
|
251
|
+
else:
|
|
252
|
+
ok(tier, f"Body has {len(section_headers)} section header(s)")
|
|
253
|
+
|
|
254
|
+
# File reference existence checks
|
|
255
|
+
# Strip fenced code blocks first — paths inside ``` are examples, not live references
|
|
256
|
+
body_no_fences = re.sub(r"```[\s\S]*?```", "", body)
|
|
257
|
+
refs = re.findall(r"(?:references|scripts)/[\w./-]+", body_no_fences)
|
|
258
|
+
for ref in refs:
|
|
259
|
+
# Skip cross-skill paths (preceded by / or ${ anywhere in the body)
|
|
260
|
+
escaped = re.escape(ref)
|
|
261
|
+
if re.search(r"[/$]" + escaped, body_no_fences):
|
|
262
|
+
continue
|
|
263
|
+
# Skip example prose: line contains illustrative markers
|
|
264
|
+
ref_line = next((l for l in body_no_fences.split("\n") if ref in l), "")
|
|
265
|
+
if re.search(r"\b(examples?|e\.g\.|such as|like `|would be|illustrat)\b", ref_line, re.IGNORECASE):
|
|
266
|
+
continue
|
|
267
|
+
ref_path = skill_dir / ref
|
|
268
|
+
if not ref_path.exists():
|
|
269
|
+
warn(tier, f"Referenced file does not exist: {ref}")
|
|
270
|
+
|
|
271
|
+
# ── Tier 4 — CLEO Integration ──────────────────────────────────────
|
|
272
|
+
tier = 4
|
|
273
|
+
|
|
274
|
+
if manifest_path:
|
|
275
|
+
manifest_file = Path(manifest_path).resolve()
|
|
276
|
+
if not manifest_file.exists():
|
|
277
|
+
error(tier, f"Manifest file not found: {manifest_path}")
|
|
278
|
+
else:
|
|
279
|
+
try:
|
|
280
|
+
manifest_data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
281
|
+
except json.JSONDecodeError as e:
|
|
282
|
+
error(tier, f"Manifest is not valid JSON: {e}")
|
|
283
|
+
manifest_data = None
|
|
284
|
+
|
|
285
|
+
if manifest_data is not None:
|
|
286
|
+
skills_list = manifest_data.get("skills", [])
|
|
287
|
+
matching = [s for s in skills_list if s.get("name") == skill_name]
|
|
288
|
+
|
|
289
|
+
if not matching:
|
|
290
|
+
warn(tier, f"Skill '{skill_name}' not found in manifest.json skills[]")
|
|
291
|
+
else:
|
|
292
|
+
ok(tier, f"Skill '{skill_name}' found in manifest.json")
|
|
293
|
+
entry = matching[0]
|
|
294
|
+
for field in MANIFEST_REQUIRED_FIELDS:
|
|
295
|
+
if field not in entry:
|
|
296
|
+
warn(tier, f"Manifest entry missing required field: '{field}'")
|
|
297
|
+
|
|
298
|
+
if dispatch_config_path:
|
|
299
|
+
dc_file = Path(dispatch_config_path).resolve()
|
|
300
|
+
if not dc_file.exists():
|
|
301
|
+
error(tier, f"Dispatch config file not found: {dispatch_config_path}")
|
|
302
|
+
else:
|
|
303
|
+
try:
|
|
304
|
+
dc_data = json.loads(dc_file.read_text(encoding="utf-8"))
|
|
305
|
+
except json.JSONDecodeError as e:
|
|
306
|
+
error(tier, f"Dispatch config is not valid JSON: {e}")
|
|
307
|
+
dc_data = None
|
|
308
|
+
|
|
309
|
+
if dc_data is not None:
|
|
310
|
+
overrides = dc_data.get("skill_overrides", {})
|
|
311
|
+
if skill_name not in overrides:
|
|
312
|
+
warn(tier, f"Skill '{skill_name}' not found in dispatch-config.json skill_overrides")
|
|
313
|
+
else:
|
|
314
|
+
ok(tier, f"Skill '{skill_name}' found in dispatch-config.json")
|
|
315
|
+
|
|
316
|
+
# ── Tier 5 — Provider Compatibility ─────────────────────────────────
|
|
317
|
+
tier = 5
|
|
318
|
+
|
|
319
|
+
if provider_map_path:
|
|
320
|
+
pm_file = Path(provider_map_path).resolve()
|
|
321
|
+
if not pm_file.exists():
|
|
322
|
+
error(tier, f"Provider map file not found: {provider_map_path}")
|
|
323
|
+
else:
|
|
324
|
+
try:
|
|
325
|
+
pm_data = json.loads(pm_file.read_text(encoding="utf-8"))
|
|
326
|
+
except json.JSONDecodeError as e:
|
|
327
|
+
error(tier, f"Provider map is not valid JSON: {e}")
|
|
328
|
+
pm_data = None
|
|
329
|
+
|
|
330
|
+
if pm_data is not None:
|
|
331
|
+
# Check if skill is referenced anywhere in the provider map
|
|
332
|
+
pm_text = json.dumps(pm_data)
|
|
333
|
+
if skill_name not in pm_text:
|
|
334
|
+
warn(tier, f"Skill '{skill_name}' not referenced in provider-skills-map.json")
|
|
335
|
+
else:
|
|
336
|
+
ok(tier, f"Skill '{skill_name}' found in provider-skills-map.json")
|
|
337
|
+
|
|
338
|
+
return results, errors, warnings
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _print_report(skill_name, results, errors, warnings):
|
|
342
|
+
"""Print the structured validation report."""
|
|
343
|
+
print(f"\n=== CLEO Skill Validator: {skill_name} ===\n")
|
|
344
|
+
|
|
345
|
+
tier_names = {
|
|
346
|
+
1: "Tier 1 — Structure",
|
|
347
|
+
2: "Tier 2 — Frontmatter Quality",
|
|
348
|
+
3: "Tier 3 — Body Quality",
|
|
349
|
+
4: "Tier 4 — CLEO Integration",
|
|
350
|
+
5: "Tier 5 — Provider Compatibility",
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
current_tier = None
|
|
354
|
+
for tier_num, severity, msg in results:
|
|
355
|
+
if tier_num != current_tier:
|
|
356
|
+
current_tier = tier_num
|
|
357
|
+
print(f"{tier_names.get(tier_num, f'Tier {tier_num}')}")
|
|
358
|
+
|
|
359
|
+
if severity == "OK":
|
|
360
|
+
print(f" \u2705 {msg}")
|
|
361
|
+
elif severity == "ERROR":
|
|
362
|
+
print(f" \u274c ERROR: {msg}")
|
|
363
|
+
elif severity == "WARN":
|
|
364
|
+
print(f" \u26a0\ufe0f WARN: {msg}")
|
|
365
|
+
|
|
366
|
+
print(f"\n=== SUMMARY ===")
|
|
367
|
+
print(f"Errors: {errors}")
|
|
368
|
+
print(f"Warnings: {warnings}")
|
|
369
|
+
|
|
370
|
+
if errors > 0:
|
|
371
|
+
print(f"Result: FAIL")
|
|
372
|
+
elif warnings > 0:
|
|
373
|
+
print(f"Result: PASS (with warnings)")
|
|
374
|
+
else:
|
|
375
|
+
print(f"Result: PASS")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def main():
|
|
379
|
+
parser = argparse.ArgumentParser(
|
|
380
|
+
description="CLEO Skill Validator — Full compliance gauntlet"
|
|
381
|
+
)
|
|
382
|
+
parser.add_argument("skill_dir", help="Path to the skill directory to validate")
|
|
383
|
+
parser.add_argument("--manifest", help="Path to manifest.json for CLEO integration check")
|
|
384
|
+
parser.add_argument("--dispatch-config", help="Path to dispatch-config.json for dispatch override check")
|
|
385
|
+
parser.add_argument("--provider-map", help="Path to provider-skills-map.json for provider compatibility check")
|
|
386
|
+
parser.add_argument("--json", action="store_true", help="Output results as JSON instead of human-readable text")
|
|
387
|
+
|
|
388
|
+
args = parser.parse_args()
|
|
389
|
+
|
|
390
|
+
skill_path = Path(args.skill_dir).resolve()
|
|
391
|
+
if not skill_path.is_dir():
|
|
392
|
+
print(f"Error: '{args.skill_dir}' is not a directory", file=sys.stderr)
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
|
|
395
|
+
skill_name = skill_path.name
|
|
396
|
+
results, errors, warnings = validate_skill(
|
|
397
|
+
skill_path,
|
|
398
|
+
manifest_path=args.manifest,
|
|
399
|
+
dispatch_config_path=args.dispatch_config,
|
|
400
|
+
provider_map_path=args.provider_map,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if getattr(args, "json"):
|
|
404
|
+
output = {
|
|
405
|
+
"skill_name": skill_name,
|
|
406
|
+
"results": [
|
|
407
|
+
{"tier": t, "severity": s, "message": m}
|
|
408
|
+
for t, s, m in results
|
|
409
|
+
],
|
|
410
|
+
"errors": errors,
|
|
411
|
+
"warnings": warnings,
|
|
412
|
+
"passed": errors == 0,
|
|
413
|
+
}
|
|
414
|
+
print(json.dumps(output, indent=2))
|
|
415
|
+
else:
|
|
416
|
+
_print_report(skill_name, results, errors, warnings)
|
|
417
|
+
|
|
418
|
+
sys.exit(1 if errors > 0 else 0)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if __name__ == "__main__":
|
|
422
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/snapshot.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/snapshot.json
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/snapshot.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/snapshot.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/snapshot.json
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/snapshot.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/snapshot.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/migration.sql
RENAMED
|
File without changes
|
/package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/snapshot.json
RENAMED
|
File without changes
|