@cleocode/cleo 2026.3.20 → 2026.3.22

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 (150) hide show
  1. package/dist/cli/index.js +39394 -38817
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/mcp/index.js +35841 -36702
  4. package/dist/mcp/index.js.map +4 -4
  5. package/drizzle-brain.config.ts +7 -0
  6. package/drizzle-nexus.config.ts +7 -0
  7. package/drizzle-tasks.config.ts +7 -0
  8. package/migrations/drizzle-brain/20260301230215_workable_spitfire/migration.sql +68 -0
  9. package/migrations/drizzle-brain/20260301230215_workable_spitfire/snapshot.json +651 -0
  10. package/migrations/drizzle-brain/20260302050325_unknown_justin_hammer/migration.sql +23 -0
  11. package/migrations/drizzle-brain/20260302050325_unknown_justin_hammer/snapshot.json +884 -0
  12. package/migrations/drizzle-brain/20260302061755_unusual_jamie_braddock/migration.sql +2 -0
  13. package/migrations/drizzle-brain/20260302061755_unusual_jamie_braddock/snapshot.json +908 -0
  14. package/migrations/drizzle-brain/20260302193548_luxuriant_glorian/migration.sql +20 -0
  15. package/migrations/drizzle-brain/20260302193548_luxuriant_glorian/snapshot.json +1078 -0
  16. package/migrations/drizzle-brain/20260304045002_white_thunderbolt_ross/migration.sql +16 -0
  17. package/migrations/drizzle-brain/20260304045002_white_thunderbolt_ross/snapshot.json +1233 -0
  18. package/migrations/drizzle-nexus/20260305070805_quick_ted_forrester/migration.sql +46 -0
  19. package/migrations/drizzle-nexus/20260305070805_quick_ted_forrester/snapshot.json +461 -0
  20. package/migrations/drizzle-tasks/20260308024513_oval_king_bedlam/migration.sql +32 -0
  21. package/migrations/drizzle-tasks/20260308024513_oval_king_bedlam/snapshot.json +3727 -0
  22. package/package.json +14 -4
  23. package/packages/ct-skills/skills/ct-cleo/SKILL.md +344 -81
  24. package/packages/ct-skills/skills/ct-grade/SKILL.md +20 -4
  25. package/packages/ct-skills/skills/ct-grade/agents/analysis-reporter.md +203 -0
  26. package/packages/ct-skills/skills/ct-grade/agents/blind-comparator.md +157 -0
  27. package/packages/ct-skills/skills/ct-grade/agents/scenario-runner.md +134 -0
  28. package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_grade_review.py +1138 -0
  29. package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_grade_viewer.py +544 -0
  30. package/packages/ct-skills/skills/ct-grade/eval-viewer/generate_review.py +283 -0
  31. package/packages/ct-skills/skills/ct-grade/eval-viewer/grade-review.html +1574 -0
  32. package/packages/ct-skills/skills/ct-grade/eval-viewer/viewer.html +219 -0
  33. package/packages/ct-skills/skills/ct-grade/evals/evals.json +94 -0
  34. package/packages/ct-skills/skills/ct-grade/references/ab-test-methodology.md +150 -0
  35. package/packages/ct-skills/skills/ct-grade/references/domains.md +137 -0
  36. package/packages/ct-skills/skills/ct-grade/references/grade-spec.md +236 -0
  37. package/packages/ct-skills/skills/ct-grade/references/scenario-playbook.md +234 -0
  38. package/packages/ct-skills/skills/ct-grade/references/token-tracking.md +120 -0
  39. package/packages/ct-skills/skills/ct-grade/scripts/audit_analyzer.py +279 -0
  40. package/packages/ct-skills/skills/ct-grade/scripts/generate_report.py +283 -0
  41. package/packages/ct-skills/skills/ct-grade/scripts/run_ab_test.py +504 -0
  42. package/packages/ct-skills/skills/ct-grade/scripts/run_all.py +287 -0
  43. package/packages/ct-skills/skills/ct-grade/scripts/setup_run.py +183 -0
  44. package/packages/ct-skills/skills/ct-grade/scripts/token_tracker.py +630 -0
  45. package/packages/ct-skills/skills/ct-grade-v2-1/SKILL.md +237 -0
  46. package/packages/ct-skills/skills/ct-grade-v2-1/agents/analysis-reporter.md +203 -0
  47. package/packages/ct-skills/skills/ct-grade-v2-1/agents/blind-comparator.md +157 -0
  48. package/packages/ct-skills/skills/ct-grade-v2-1/agents/scenario-runner.md +179 -0
  49. package/packages/ct-skills/skills/ct-grade-v2-1/evals/evals.json +74 -0
  50. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/build_op_stats.py +174 -0
  51. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/eval-analysis.json +41 -0
  52. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/eval-report.md +34 -0
  53. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/generate_grade_review.py +1023 -0
  54. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/generate_grade_viewer.py +548 -0
  55. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/grade-review-eval.html +613 -0
  56. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/grade-review.html +1532 -0
  57. package/packages/ct-skills/skills/ct-grade-v2-1/grade-viewer/viewer.html +620 -0
  58. package/packages/ct-skills/skills/ct-grade-v2-1/manifest-entry.json +31 -0
  59. package/packages/ct-skills/skills/ct-grade-v2-1/references/ab-testing.md +233 -0
  60. package/packages/ct-skills/skills/ct-grade-v2-1/references/domains-ssot.md +156 -0
  61. package/packages/ct-skills/skills/ct-grade-v2-1/references/grade-spec-v2.md +167 -0
  62. package/packages/ct-skills/skills/ct-grade-v2-1/references/playbook-v2.md +393 -0
  63. package/packages/ct-skills/skills/ct-grade-v2-1/references/token-tracking.md +202 -0
  64. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/generate_report.py +419 -0
  65. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/run_ab_test.py +493 -0
  66. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/run_scenario.py +396 -0
  67. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/setup_run.py +207 -0
  68. package/packages/ct-skills/skills/ct-grade-v2-1/scripts/token_tracker.py +175 -0
  69. package/packages/ct-skills/skills/ct-orchestrator/SKILL.md +1 -29
  70. package/packages/ct-skills/skills/ct-orchestrator/manifest-entry.json +19 -0
  71. package/packages/ct-skills/skills/ct-skill-creator/SKILL.md +0 -12
  72. package/packages/ct-skills/skills/ct-skill-creator/agents/analyzer.md +276 -0
  73. package/packages/ct-skills/skills/ct-skill-creator/agents/comparator.md +204 -0
  74. package/packages/ct-skills/skills/ct-skill-creator/agents/grader.md +225 -0
  75. package/packages/ct-skills/skills/ct-skill-creator/assets/eval_review.html +146 -0
  76. package/packages/ct-skills/skills/ct-skill-creator/eval-viewer/generate_review.py +471 -0
  77. package/packages/ct-skills/skills/ct-skill-creator/eval-viewer/viewer.html +1325 -0
  78. package/packages/ct-skills/skills/ct-skill-creator/manifest-entry.json +17 -0
  79. package/packages/ct-skills/skills/ct-skill-creator/references/dynamic-context.md +228 -0
  80. package/packages/ct-skills/skills/ct-skill-creator/references/frontmatter.md +83 -0
  81. package/packages/ct-skills/skills/ct-skill-creator/references/invocation-control.md +165 -0
  82. package/packages/ct-skills/skills/ct-skill-creator/references/provider-deployment.md +175 -0
  83. package/packages/ct-skills/skills/ct-skill-creator/references/schemas.md +430 -0
  84. package/packages/ct-skills/skills/ct-skill-creator/scripts/__init__.py +1 -0
  85. package/packages/ct-skills/skills/ct-skill-creator/scripts/aggregate_benchmark.py +401 -0
  86. package/packages/ct-skills/skills/ct-skill-creator/scripts/generate_report.py +326 -0
  87. package/packages/ct-skills/skills/ct-skill-creator/scripts/improve_description.py +247 -0
  88. package/packages/ct-skills/skills/ct-skill-creator/scripts/run_eval.py +310 -0
  89. package/packages/ct-skills/skills/ct-skill-creator/scripts/run_loop.py +328 -0
  90. package/packages/ct-skills/skills/ct-skill-creator/scripts/utils.py +47 -0
  91. package/packages/ct-skills/skills/ct-skill-validator/SKILL.md +178 -0
  92. package/packages/ct-skills/skills/ct-skill-validator/agents/ecosystem-checker.md +151 -0
  93. package/packages/ct-skills/skills/ct-skill-validator/assets/valid-skill-example.md +13 -0
  94. package/packages/ct-skills/skills/ct-skill-validator/evals/eval_set.json +14 -0
  95. package/packages/ct-skills/skills/ct-skill-validator/evals/evals.json +52 -0
  96. package/packages/ct-skills/skills/ct-skill-validator/manifest-entry.json +20 -0
  97. package/packages/ct-skills/skills/ct-skill-validator/references/cleo-ecosystem-rules.md +163 -0
  98. package/packages/ct-skills/skills/ct-skill-validator/references/validation-rules.md +168 -0
  99. package/packages/ct-skills/skills/ct-skill-validator/scripts/__init__.py +0 -0
  100. package/packages/ct-skills/skills/ct-skill-validator/scripts/audit_body.py +242 -0
  101. package/packages/ct-skills/skills/ct-skill-validator/scripts/check_ecosystem.py +169 -0
  102. package/packages/ct-skills/skills/ct-skill-validator/scripts/check_manifest.py +172 -0
  103. package/packages/ct-skills/skills/ct-skill-validator/scripts/generate_validation_report.py +442 -0
  104. package/packages/ct-skills/skills/ct-skill-validator/scripts/validate.py +422 -0
  105. /package/{drizzle → migrations/drizzle-tasks}/20260224040019_baseline/migration.sql +0 -0
  106. /package/{drizzle → migrations/drizzle-tasks}/20260224040019_baseline/snapshot.json +0 -0
  107. /package/{drizzle → migrations/drizzle-tasks}/20260224040238_add-audit-log/migration.sql +0 -0
  108. /package/{drizzle → migrations/drizzle-tasks}/20260224040238_add-audit-log/snapshot.json +0 -0
  109. /package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/migration.sql +0 -0
  110. /package/{drizzle → migrations/drizzle-tasks}/20260224144602_closed_grim_reaper/snapshot.json +0 -0
  111. /package/{drizzle → migrations/drizzle-tasks}/20260225024442_sync-lifecycle-enums-and-arch-decisions/migration.sql +0 -0
  112. /package/{drizzle → migrations/drizzle-tasks}/20260225024442_sync-lifecycle-enums-and-arch-decisions/snapshot.json +0 -0
  113. /package/{drizzle → migrations/drizzle-tasks}/20260227014821_adr-system-and-status-registry/migration.sql +0 -0
  114. /package/{drizzle → migrations/drizzle-tasks}/20260227014821_adr-system-and-status-registry/snapshot.json +0 -0
  115. /package/{drizzle → migrations/drizzle-tasks}/20260227021231_add-cancelled-pipeline-status/migration.sql +0 -0
  116. /package/{drizzle → migrations/drizzle-tasks}/20260227021231_add-cancelled-pipeline-status/snapshot.json +0 -0
  117. /package/{drizzle → migrations/drizzle-tasks}/20260227022417_adr-cognitive-search-fields/migration.sql +0 -0
  118. /package/{drizzle → migrations/drizzle-tasks}/20260227022417_adr-cognitive-search-fields/snapshot.json +0 -0
  119. /package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/migration.sql +0 -0
  120. /package/{drizzle → migrations/drizzle-tasks}/20260227172236_freezing_grey_gargoyle/snapshot.json +0 -0
  121. /package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/migration.sql +0 -0
  122. /package/{drizzle → migrations/drizzle-tasks}/20260227183444_fix-orphaned-parent-ids/snapshot.json +0 -0
  123. /package/{drizzle → migrations/drizzle-tasks}/20260227183521_parent-id-on-delete-set-null/migration.sql +0 -0
  124. /package/{drizzle → migrations/drizzle-tasks}/20260227183521_parent-id-on-delete-set-null/snapshot.json +0 -0
  125. /package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/migration.sql +0 -0
  126. /package/{drizzle → migrations/drizzle-tasks}/20260227200430_numerous_mysterio/snapshot.json +0 -0
  127. /package/{drizzle → migrations/drizzle-tasks}/20260227235745_add-audit-log-dispatch-columns/migration.sql +0 -0
  128. /package/{drizzle → migrations/drizzle-tasks}/20260227235745_add-audit-log-dispatch-columns/snapshot.json +0 -0
  129. /package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/migration.sql +0 -0
  130. /package/{drizzle → migrations/drizzle-tasks}/20260301053344_careless_changeling/snapshot.json +0 -0
  131. /package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/migration.sql +0 -0
  132. /package/{drizzle → migrations/drizzle-tasks}/20260301175940_futuristic_eternity/snapshot.json +0 -0
  133. /package/{drizzle → migrations/drizzle-tasks}/20260301180528_update-task-relations-check-constraint/migration.sql +0 -0
  134. /package/{drizzle → migrations/drizzle-tasks}/20260301180528_update-task-relations-check-constraint/snapshot.json +0 -0
  135. /package/{drizzle → migrations/drizzle-tasks}/20260302163443_free_silk_fever/migration.sql +0 -0
  136. /package/{drizzle → migrations/drizzle-tasks}/20260302163443_free_silk_fever/snapshot.json +0 -0
  137. /package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/migration.sql +0 -0
  138. /package/{drizzle → migrations/drizzle-tasks}/20260302163457_robust_johnny_storm/snapshot.json +0 -0
  139. /package/{drizzle → migrations/drizzle-tasks}/20260302163511_late_sphinx/migration.sql +0 -0
  140. /package/{drizzle → migrations/drizzle-tasks}/20260302163511_late_sphinx/snapshot.json +0 -0
  141. /package/{drizzle → migrations/drizzle-tasks}/20260305011924_cheerful_mongu/migration.sql +0 -0
  142. /package/{drizzle → migrations/drizzle-tasks}/20260305011924_cheerful_mongu/snapshot.json +0 -0
  143. /package/{drizzle → migrations/drizzle-tasks}/20260305203927_demonic_storm/migration.sql +0 -0
  144. /package/{drizzle → migrations/drizzle-tasks}/20260305203927_demonic_storm/snapshot.json +0 -0
  145. /package/{drizzle → migrations/drizzle-tasks}/20260306001243_spooky_rage/migration.sql +0 -0
  146. /package/{drizzle → migrations/drizzle-tasks}/20260306001243_spooky_rage/snapshot.json +0 -0
  147. /package/{drizzle → migrations/drizzle-tasks}/20260306193138_young_morbius/migration.sql +0 -0
  148. /package/{drizzle → migrations/drizzle-tasks}/20260306193138_young_morbius/snapshot.json +0 -0
  149. /package/{drizzle → migrations/drizzle-tasks}/20260306194959_sticky_captain_flint/migration.sql +0 -0
  150. /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()