@intentsolutionsio/skill-creator 5.0.3 → 5.0.6
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/package.json +1 -1
- package/scripts/validate-skill.py +61 -1123
- package/skills/skill-creator/agents/analyzer.md +28 -1
- package/skills/skill-creator/agents/comparator.md +28 -1
- package/skills/skill-creator/agents/grader.md +28 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-312.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-312.pyc +0 -0
package/package.json
CHANGED
|
@@ -1,1087 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Skill Validator
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
Skill Validator — thin delegating wrapper around the repo's main validator.
|
|
4
|
+
|
|
5
|
+
This script used to carry its own 1,155-line validation + 100-point grading
|
|
6
|
+
implementation. That copy diverged from the authoritative validator (it was
|
|
7
|
+
missing the disallowed-tools and visibility fields, among others), so it now
|
|
8
|
+
delegates to scripts/validate-skills-schema.py at the repository root.
|
|
9
|
+
The main validator's marketplace tier carries the IS 100-point rubric.
|
|
10
|
+
|
|
11
|
+
CLI surface (preserved from the previous implementation):
|
|
12
|
+
python3 validate-skill.py path/to/SKILL.md # Standard (default)
|
|
13
|
+
python3 validate-skill.py --enterprise path/to/SKILL.md # Marketplace tier
|
|
14
|
+
python3 validate-skill.py --grade path/to/SKILL.md # Rubric grading
|
|
15
|
+
python3 validate-skill.py --grade --json path/to/SKILL.md
|
|
16
|
+
python3 validate-skill.py --json path/to/SKILL.md
|
|
17
|
+
|
|
18
|
+
Exit codes: 0 = valid / passing, 1 = errors (unchanged).
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
import argparse
|
|
19
|
-
import
|
|
20
|
-
import re
|
|
22
|
+
import subprocess
|
|
21
23
|
import sys
|
|
22
24
|
from pathlib import Path
|
|
23
|
-
from typing import Any, Dict, List, Tuple
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
import yaml
|
|
27
|
-
except ImportError:
|
|
28
|
-
print("ERROR: pyyaml required. Install: pip install pyyaml", file=sys.stderr)
|
|
29
|
-
sys.exit(1)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# === CONSTANTS ===
|
|
33
|
-
|
|
34
|
-
VALID_TOOLS = {
|
|
35
|
-
"Read",
|
|
36
|
-
"Write",
|
|
37
|
-
"Edit",
|
|
38
|
-
"Bash",
|
|
39
|
-
"Glob",
|
|
40
|
-
"Grep",
|
|
41
|
-
"WebFetch",
|
|
42
|
-
"WebSearch",
|
|
43
|
-
"Task",
|
|
44
|
-
"NotebookEdit",
|
|
45
|
-
"AskUserQuestion",
|
|
46
|
-
"Skill",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
KNOWN_FRONTMATTER_FIELDS = {
|
|
50
|
-
# AgentSkills.io spec
|
|
51
|
-
"name",
|
|
52
|
-
"description",
|
|
53
|
-
"license",
|
|
54
|
-
"compatibility",
|
|
55
|
-
"metadata",
|
|
56
|
-
"allowed-tools",
|
|
57
|
-
# Top-level identity fields (marketplace standard)
|
|
58
|
-
"version",
|
|
59
|
-
"author",
|
|
60
|
-
"compatible-with",
|
|
61
|
-
"tags",
|
|
62
|
-
# Claude Code extensions
|
|
63
|
-
"argument-hint",
|
|
64
|
-
"disable-model-invocation",
|
|
65
|
-
"user-invocable",
|
|
66
|
-
"model",
|
|
67
|
-
"context",
|
|
68
|
-
"agent",
|
|
69
|
-
"hooks",
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
DEPRECATED_FIELDS = {
|
|
73
|
-
"when_to_use": "Move content to description",
|
|
74
|
-
"mode": "Use disable-model-invocation instead",
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
VALID_PLATFORMS = {
|
|
78
|
-
"claude-code",
|
|
79
|
-
"codex",
|
|
80
|
-
"openclaw",
|
|
81
|
-
"aider",
|
|
82
|
-
"continue",
|
|
83
|
-
"cursor",
|
|
84
|
-
"windsurf",
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
RE_FRONTMATTER = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
|
|
88
|
-
RE_FIRST_PERSON = re.compile(r"\b(I can|I will|I'm going to|I help|I'm)\b", re.IGNORECASE)
|
|
89
|
-
RE_SECOND_PERSON = re.compile(r"\b(You can|You should|You will)\b", re.IGNORECASE)
|
|
90
|
-
RE_HARDCODED_MODEL = re.compile(r"claude-[\w]+-\d{8}")
|
|
91
|
-
RE_CONSECUTIVE_HYPHENS = re.compile(r"--")
|
|
92
|
-
RE_WINDOWS_PATH = re.compile(r"[A-Za-z]:\\\\", re.IGNORECASE)
|
|
93
|
-
RE_ARGUMENTS = re.compile(r"\$ARGUMENTS|\$\d+|\$ARGUMENTS\[\d+\]")
|
|
94
|
-
RE_DYNAMIC_CONTEXT = re.compile(r"!`[^`]+`")
|
|
95
|
-
|
|
96
|
-
RE_XML_TAG = re.compile(r"[<>]")
|
|
97
|
-
RE_TIME_SENSITIVE = [
|
|
98
|
-
re.compile(r"\b(20\d{2}[-/]\d{2}[-/]\d{2})\b"), # dates like 2025-01-01
|
|
99
|
-
re.compile(r"\b(v\d+\.\d+\.\d+)\b", re.IGNORECASE), # version numbers like v1.2.3
|
|
100
|
-
re.compile(
|
|
101
|
-
r"\b(as of|since|after|before) (January|February|March|April|May|June|July|August|September|October|November|December)\b",
|
|
102
|
-
re.IGNORECASE,
|
|
103
|
-
),
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
ABSOLUTE_PATH_PATTERNS = [
|
|
107
|
-
(re.compile(r"/home/\w+/"), "/home/..."),
|
|
108
|
-
(re.compile(r"/Users/\w+/"), "/Users/..."),
|
|
109
|
-
(re.compile(r"[A-Za-z]:\\\\Users\\\\", re.IGNORECASE), "C:\\Users\\..."),
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def parse_frontmatter(content: str) -> Tuple[dict, str]:
|
|
114
|
-
"""Parse YAML frontmatter from SKILL.md content."""
|
|
115
|
-
m = RE_FRONTMATTER.match(content)
|
|
116
|
-
if not m:
|
|
117
|
-
raise ValueError("Invalid or absent YAML frontmatter (missing --- delimiters)")
|
|
118
|
-
front_str, body = m.groups()
|
|
119
|
-
try:
|
|
120
|
-
data = yaml.safe_load(front_str) or {}
|
|
121
|
-
except yaml.YAMLError as e:
|
|
122
|
-
raise ValueError(f"YAML parse error: {e}")
|
|
123
|
-
return data, body
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def validate_name(fm: dict, path: Path, enterprise: bool = False) -> Tuple[List[str], List[str]]:
|
|
127
|
-
"""Validate the name field."""
|
|
128
|
-
errors, warnings = [], []
|
|
129
|
-
if "name" not in fm:
|
|
130
|
-
if enterprise:
|
|
131
|
-
errors.append("Missing required field: 'name'")
|
|
132
|
-
else:
|
|
133
|
-
warnings.append(f"INFO: 'name' not set - defaults to directory name '{path.parent.name}'")
|
|
134
|
-
return errors, warnings
|
|
135
|
-
|
|
136
|
-
name = str(fm["name"]).strip()
|
|
137
|
-
if not name:
|
|
138
|
-
errors.append("'name' must be non-empty")
|
|
139
|
-
return errors, warnings
|
|
140
|
-
|
|
141
|
-
if len(name) > 64:
|
|
142
|
-
errors.append(f"'name' exceeds 64 characters ({len(name)})")
|
|
143
|
-
|
|
144
|
-
if not re.match(r"^[a-z]([a-z0-9-]*[a-z0-9])?$", name):
|
|
145
|
-
errors.append(f"'name' must be kebab-case (lowercase, hyphens): '{name}'")
|
|
146
|
-
elif RE_CONSECUTIVE_HYPHENS.search(name):
|
|
147
|
-
errors.append(f"'name' has consecutive hyphens: '{name}'")
|
|
148
|
-
|
|
149
|
-
if name.startswith("-") or name.endswith("-"):
|
|
150
|
-
errors.append(f"'name' must not start or end with hyphen: '{name}'")
|
|
151
|
-
|
|
152
|
-
if name != path.parent.name:
|
|
153
|
-
warnings.append(f"'name' '{name}' differs from directory '{path.parent.name}'")
|
|
154
|
-
|
|
155
|
-
if RE_XML_TAG.search(name):
|
|
156
|
-
errors.append(f"'name' must not contain XML tags (< or >): '{name}'")
|
|
157
|
-
|
|
158
|
-
return errors, warnings
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def validate_description(fm: dict, enterprise: bool = False) -> Tuple[List[str], List[str]]:
|
|
162
|
-
"""Validate the description field."""
|
|
163
|
-
errors, warnings = [], []
|
|
164
|
-
if "description" not in fm:
|
|
165
|
-
if enterprise:
|
|
166
|
-
errors.append("Missing required field: 'description'")
|
|
167
|
-
else:
|
|
168
|
-
warnings.append("'description' is recommended for discovery - add trigger phrases and use-cases")
|
|
169
|
-
return errors, warnings
|
|
170
|
-
|
|
171
|
-
desc = str(fm["description"]).strip()
|
|
172
|
-
if not desc:
|
|
173
|
-
errors.append("'description' must be non-empty")
|
|
174
|
-
return errors, warnings
|
|
175
|
-
|
|
176
|
-
if len(desc) > 1024:
|
|
177
|
-
errors.append(f"'description' exceeds 1024 characters ({len(desc)})")
|
|
178
|
-
if len(desc) < 20:
|
|
179
|
-
warnings.append(f"'description' is very short ({len(desc)} chars) - add keywords for discovery")
|
|
180
|
-
if len(desc) > 500:
|
|
181
|
-
warnings.append(f"'description' is long ({len(desc)} chars) - impacts token budget")
|
|
182
|
-
|
|
183
|
-
if RE_FIRST_PERSON.search(desc):
|
|
184
|
-
if enterprise:
|
|
185
|
-
errors.append("'description' must not use first person (I can, I will, I'm, I help)")
|
|
186
|
-
else:
|
|
187
|
-
warnings.append("'description' should not use first person (I can, I will, I'm, I help)")
|
|
188
|
-
if RE_SECOND_PERSON.search(desc):
|
|
189
|
-
if enterprise:
|
|
190
|
-
errors.append("'description' must not use second person (You can, You should, You will)")
|
|
191
|
-
else:
|
|
192
|
-
warnings.append("'description' should not use second person (You can, You should, You will)")
|
|
193
|
-
|
|
194
|
-
if RE_XML_TAG.search(desc):
|
|
195
|
-
errors.append("'description' must not contain XML tags (< or >)")
|
|
196
|
-
|
|
197
|
-
return errors, warnings
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def validate_tools(fm: dict, enterprise: bool) -> Tuple[List[str], List[str]]:
|
|
201
|
-
"""Validate allowed-tools field."""
|
|
202
|
-
errors, warnings = [], []
|
|
203
|
-
if "allowed-tools" not in fm:
|
|
204
|
-
if enterprise:
|
|
205
|
-
warnings.append("'allowed-tools' not set (Enterprise recommends scoped tools)")
|
|
206
|
-
return errors, warnings
|
|
207
|
-
|
|
208
|
-
tools_val = fm["allowed-tools"]
|
|
209
|
-
if isinstance(tools_val, list):
|
|
210
|
-
tools = [str(t).strip() for t in tools_val]
|
|
211
|
-
else:
|
|
212
|
-
raw = str(tools_val)
|
|
213
|
-
tools = re.findall(r"[A-Za-z_][\w-]*(?:\([^)]*\))?", raw)
|
|
214
|
-
|
|
215
|
-
if not tools or tools == [""]:
|
|
216
|
-
errors.append("'allowed-tools' is empty")
|
|
217
|
-
return errors, warnings
|
|
218
|
-
|
|
219
|
-
for tool in tools:
|
|
220
|
-
if ":" in tool and "(" not in tool:
|
|
221
|
-
continue # MCP tool reference
|
|
222
|
-
base_tool = tool.split("(")[0].strip()
|
|
223
|
-
if base_tool not in VALID_TOOLS:
|
|
224
|
-
errors.append(f"Unknown tool: '{base_tool}'")
|
|
225
|
-
if "(" in tool and not tool.endswith(")"):
|
|
226
|
-
errors.append(f"Invalid tool scope syntax: '{tool}'")
|
|
227
|
-
|
|
228
|
-
if "Bash" in tools:
|
|
229
|
-
if enterprise:
|
|
230
|
-
errors.append("Unscoped 'Bash' forbidden in Enterprise tier - use Bash(git:*) etc.")
|
|
231
|
-
else:
|
|
232
|
-
warnings.append("Unscoped 'Bash' - consider scoping: Bash(git:*), Bash(npm:*)")
|
|
233
|
-
|
|
234
|
-
return errors, warnings
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def validate_optional_fields(fm: dict) -> Tuple[List[str], List[str]]:
|
|
238
|
-
"""Validate optional and extension fields."""
|
|
239
|
-
errors, warnings = [], []
|
|
240
|
-
|
|
241
|
-
# Model validation
|
|
242
|
-
if "model" in fm:
|
|
243
|
-
model = str(fm["model"]).strip()
|
|
244
|
-
valid_models = {"inherit", "sonnet", "haiku", "opus"}
|
|
245
|
-
if model not in valid_models and not model.startswith("claude-"):
|
|
246
|
-
warnings.append(f"'model' value '{model}' - expected: inherit, sonnet, haiku, opus, or claude-* ID")
|
|
247
|
-
if RE_HARDCODED_MODEL.match(model):
|
|
248
|
-
warnings.append(f"Hardcoded model ID '{model}' - prefer 'inherit' or short names (sonnet, opus)")
|
|
249
|
-
|
|
250
|
-
# Context + agent validation
|
|
251
|
-
if "context" in fm:
|
|
252
|
-
if str(fm["context"]) != "fork":
|
|
253
|
-
errors.append("'context' must be 'fork' if set")
|
|
254
|
-
if "agent" in fm and "context" not in fm:
|
|
255
|
-
warnings.append("'agent' set without 'context: fork' - agent requires fork context")
|
|
256
|
-
|
|
257
|
-
# Boolean fields
|
|
258
|
-
for field in ("disable-model-invocation", "user-invocable"):
|
|
259
|
-
if field in fm and not isinstance(fm[field], bool):
|
|
260
|
-
errors.append(f"'{field}' must be boolean, got: {type(fm[field]).__name__}")
|
|
261
|
-
|
|
262
|
-
# Conflicting invocation controls
|
|
263
|
-
if fm.get("disable-model-invocation") is True and fm.get("user-invocable") is False:
|
|
264
|
-
errors.append(
|
|
265
|
-
"Conflicting: 'disable-model-invocation: true' + 'user-invocable: false' "
|
|
266
|
-
"makes skill unreachable by both user and model"
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
# Compatibility length
|
|
270
|
-
if "compatibility" in fm:
|
|
271
|
-
compat = str(fm["compatibility"]).strip()
|
|
272
|
-
if len(compat) > 500:
|
|
273
|
-
errors.append(f"'compatibility' exceeds 500 characters ({len(compat)})")
|
|
274
|
-
|
|
275
|
-
# compatible-with validation
|
|
276
|
-
if "compatible-with" in fm:
|
|
277
|
-
compat_with = fm["compatible-with"]
|
|
278
|
-
if isinstance(compat_with, str):
|
|
279
|
-
platforms = [p.strip() for p in compat_with.split(",")]
|
|
280
|
-
elif isinstance(compat_with, list):
|
|
281
|
-
platforms = [str(p).strip() for p in compat_with]
|
|
282
|
-
else:
|
|
283
|
-
platforms = []
|
|
284
|
-
warnings.append("'compatible-with' should be a comma-separated string or list")
|
|
285
|
-
for p in platforms:
|
|
286
|
-
if p and p not in VALID_PLATFORMS:
|
|
287
|
-
warnings.append(f"Unknown platform in compatible-with: '{p}'")
|
|
288
25
|
|
|
289
|
-
# tags validation
|
|
290
|
-
if "tags" in fm:
|
|
291
|
-
tags = fm["tags"]
|
|
292
|
-
if not isinstance(tags, list):
|
|
293
|
-
warnings.append("'tags' should be an array/list of strings")
|
|
294
26
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
author = str(fm["author"]).strip()
|
|
304
|
-
if not author:
|
|
305
|
-
warnings.append("'author' is empty")
|
|
306
|
-
elif "@" not in author:
|
|
307
|
-
warnings.append("'author' should include email (Name <email>)")
|
|
308
|
-
|
|
309
|
-
# Deprecated fields
|
|
310
|
-
for field, msg in DEPRECATED_FIELDS.items():
|
|
311
|
-
if field in fm:
|
|
312
|
-
warnings.append(f"Deprecated field '{field}': {msg}")
|
|
313
|
-
|
|
314
|
-
# Check if author/version are nested in metadata (warn to move top-level)
|
|
315
|
-
metadata = fm.get("metadata", {})
|
|
316
|
-
if isinstance(metadata, dict):
|
|
317
|
-
for field in ("author", "version", "license", "tags"):
|
|
318
|
-
if field in metadata and field not in fm:
|
|
319
|
-
warnings.append(f"'{field}' found in metadata block - move to top-level for marketplace scoring")
|
|
320
|
-
|
|
321
|
-
# Unknown fields
|
|
322
|
-
all_known = KNOWN_FRONTMATTER_FIELDS | DEPRECATED_FIELDS.keys()
|
|
323
|
-
for field in fm:
|
|
324
|
-
if field not in all_known:
|
|
325
|
-
warnings.append(f"Unknown frontmatter field: '{field}'")
|
|
326
|
-
|
|
327
|
-
return errors, warnings
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def validate_enterprise_metadata(fm: dict) -> Tuple[List[str], List[str]]:
|
|
331
|
-
"""Enterprise tier: check author and version at top-level (or metadata fallback)."""
|
|
332
|
-
errors, warnings = [], []
|
|
333
|
-
metadata = fm.get("metadata", {})
|
|
334
|
-
if not isinstance(metadata, dict):
|
|
335
|
-
metadata = {}
|
|
336
|
-
|
|
337
|
-
# Check author (top-level preferred, metadata fallback accepted with warning)
|
|
338
|
-
has_author = "author" in fm
|
|
339
|
-
has_author_meta = "author" in metadata
|
|
340
|
-
if not has_author and not has_author_meta:
|
|
341
|
-
warnings.append("Enterprise: 'author' recommended (top-level field)")
|
|
342
|
-
elif has_author_meta and not has_author:
|
|
343
|
-
warnings.append("'author' in metadata block - move to top-level for marketplace scoring")
|
|
344
|
-
|
|
345
|
-
# Check version (top-level preferred, metadata fallback accepted with warning)
|
|
346
|
-
has_version = "version" in fm
|
|
347
|
-
has_version_meta = "version" in metadata
|
|
348
|
-
if not has_version and not has_version_meta:
|
|
349
|
-
warnings.append("Enterprise: 'version' recommended (top-level field)")
|
|
350
|
-
elif has_version_meta and not has_version:
|
|
351
|
-
warnings.append("'version' in metadata block - move to top-level for marketplace scoring")
|
|
352
|
-
|
|
353
|
-
# Check license
|
|
354
|
-
if "license" not in fm:
|
|
355
|
-
warnings.append("Enterprise: 'license' recommended (top-level field)")
|
|
356
|
-
|
|
357
|
-
return errors, warnings
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def validate_body(body: str, path: Path, enterprise: bool) -> Tuple[List[str], List[str], List[str]]:
|
|
361
|
-
"""Validate SKILL.md body content."""
|
|
362
|
-
errors, warnings, info = [], [], []
|
|
363
|
-
lines = body.splitlines()
|
|
364
|
-
line_count = len(lines)
|
|
365
|
-
word_count = len(body.split())
|
|
366
|
-
|
|
367
|
-
if line_count > 500:
|
|
368
|
-
errors.append(f"Body has {line_count} lines (max 500)")
|
|
369
|
-
elif line_count > 400:
|
|
370
|
-
warnings.append(f"Body has {line_count} lines (approaching 500 limit)")
|
|
371
|
-
|
|
372
|
-
if word_count > 5000:
|
|
373
|
-
warnings.append(f"Body has {word_count} words - consider splitting to references/")
|
|
374
|
-
|
|
375
|
-
# Strip code blocks for path checks
|
|
376
|
-
body_no_code = re.sub(r"```.*?```", "", body, flags=re.DOTALL)
|
|
377
|
-
body_no_code = re.sub(r"`[^`]+`", "", body_no_code)
|
|
378
|
-
|
|
379
|
-
for pattern, desc in ABSOLUTE_PATH_PATTERNS:
|
|
380
|
-
if pattern.search(body_no_code):
|
|
381
|
-
errors.append(f"Contains absolute path ({desc}) - use ${{CLAUDE_SKILL_DIR}}")
|
|
382
|
-
|
|
383
|
-
if RE_WINDOWS_PATH.search(body_no_code):
|
|
384
|
-
errors.append("Contains Windows-style path - use Unix paths or ${CLAUDE_SKILL_DIR}")
|
|
385
|
-
|
|
386
|
-
if not re.search(r"^# \S", body, re.MULTILINE):
|
|
387
|
-
if enterprise:
|
|
388
|
-
warnings.append("Missing H1 title (# Title)")
|
|
389
|
-
|
|
390
|
-
if enterprise:
|
|
391
|
-
instruction_headings = ("## Instructions", "## Steps", "## Your task", "## Usage", "## Workflow")
|
|
392
|
-
has_instructions = any(h in body for h in instruction_headings)
|
|
393
|
-
if not has_instructions:
|
|
394
|
-
warnings.append("Missing '## Instructions' section (or ## Steps/Your task/Usage/Workflow)")
|
|
395
|
-
else:
|
|
396
|
-
instr_match = re.search(
|
|
397
|
-
r"## (?:Instructions|Steps|Your task|Usage|Workflow)(.*?)(?=\n## |\Z)", body, re.DOTALL
|
|
398
|
-
)
|
|
399
|
-
if instr_match:
|
|
400
|
-
instr = instr_match.group(1)
|
|
401
|
-
has_steps = re.search(r"(?m)^\s*\d+\.\s+", instr) or re.search(r"(?mi)^\s*#{2,6}\s*step\s*\d+", instr)
|
|
402
|
-
if not has_steps:
|
|
403
|
-
warnings.append("Instructions should have numbered steps or ### Step N headings")
|
|
404
|
-
|
|
405
|
-
if "## Examples" not in body and "## Example" not in body:
|
|
406
|
-
warnings.append("Missing '## Examples' section")
|
|
407
|
-
|
|
408
|
-
if "## Error" not in body and "error" not in body.lower():
|
|
409
|
-
info.append("Consider adding error handling documentation")
|
|
410
|
-
|
|
411
|
-
skill_dir = path.parent.resolve()
|
|
412
|
-
refs_dir = skill_dir / "references"
|
|
413
|
-
if line_count > 300 and not refs_dir.exists():
|
|
414
|
-
warnings.append(
|
|
415
|
-
f"SKILL.md is {line_count} lines with no references/ directory - consider splitting heavy content"
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
if "{baseDir}/../" in body:
|
|
419
|
-
errors.append("Path escape detected: {baseDir}/../ - references must stay within skill directory")
|
|
420
|
-
|
|
421
|
-
nested_refs = re.findall(r"\{baseDir\}/references/\S+/\S+/", body)
|
|
422
|
-
if nested_refs:
|
|
423
|
-
warnings.append(f"Deeply nested reference paths detected: {nested_refs[0]} - keep references one level deep")
|
|
424
|
-
|
|
425
|
-
dynamic_cmds = RE_DYNAMIC_CONTEXT.findall(body)
|
|
426
|
-
if dynamic_cmds:
|
|
427
|
-
info.append(f"Dynamic context injection detected ({len(dynamic_cmds)} command(s))")
|
|
428
|
-
|
|
429
|
-
if RE_ARGUMENTS.search(body):
|
|
430
|
-
try:
|
|
431
|
-
content = path.read_text(encoding="utf-8")
|
|
432
|
-
if "argument-hint" not in content.split("---")[1]:
|
|
433
|
-
info.append("Uses $ARGUMENTS but no 'argument-hint' in frontmatter")
|
|
434
|
-
except Exception:
|
|
435
|
-
pass
|
|
436
|
-
|
|
437
|
-
# Time-sensitive information detection (skip code blocks)
|
|
438
|
-
stripped_body = re.sub(r"```[\s\S]*?```", "", body)
|
|
439
|
-
stripped_body = re.sub(r"`[^`]+`", "", stripped_body)
|
|
440
|
-
for pattern in RE_TIME_SENSITIVE:
|
|
441
|
-
if pattern.search(stripped_body):
|
|
442
|
-
warnings.append("Body may contain time-sensitive information (dates, versions) that could go stale")
|
|
27
|
+
def find_main_validator() -> Path | None:
|
|
28
|
+
"""Resolve scripts/validate-skills-schema.py by walking up from this file."""
|
|
29
|
+
current = Path(__file__).resolve().parent
|
|
30
|
+
for _ in range(10):
|
|
31
|
+
candidate = current / "scripts" / "validate-skills-schema.py"
|
|
32
|
+
if candidate.exists():
|
|
33
|
+
return candidate
|
|
34
|
+
if current.parent == current:
|
|
443
35
|
break
|
|
36
|
+
current = current.parent
|
|
37
|
+
return None
|
|
444
38
|
|
|
445
|
-
return errors, warnings, info
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
def validate_resources(body: str, path: Path) -> List[str]:
|
|
449
|
-
"""Validate that referenced resources exist."""
|
|
450
|
-
errors = []
|
|
451
|
-
skill_dir = path.parent.resolve()
|
|
452
|
-
|
|
453
|
-
for subdir in ("scripts", "references", "templates", "assets"):
|
|
454
|
-
# Check both ${CLAUDE_SKILL_DIR} and {baseDir} references
|
|
455
|
-
for pattern in [
|
|
456
|
-
rf"\$\{{CLAUDE_SKILL_DIR\}}/{subdir}/([\w\-./]+)",
|
|
457
|
-
rf"\{{baseDir\}}/{subdir}/([\w\-./]+)",
|
|
458
|
-
]:
|
|
459
|
-
for match in re.finditer(pattern, body):
|
|
460
|
-
rel_path = match.group(1)
|
|
461
|
-
full_path = skill_dir / subdir / rel_path
|
|
462
|
-
if not full_path.exists():
|
|
463
|
-
errors.append(f"Resource not found: {subdir}/{rel_path}")
|
|
464
|
-
|
|
465
|
-
return errors
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
def calculate_disclosure_score(fm: dict, body: str, path: Path) -> int:
|
|
469
|
-
"""Calculate progressive disclosure score (0-6)."""
|
|
470
|
-
score = 0
|
|
471
|
-
lines = len(body.splitlines())
|
|
472
|
-
skill_dir = path.parent.resolve()
|
|
473
|
-
|
|
474
|
-
if lines < 200:
|
|
475
|
-
score += 2
|
|
476
|
-
elif lines < 400:
|
|
477
|
-
score += 1
|
|
478
|
-
|
|
479
|
-
desc = str(fm.get("description", ""))
|
|
480
|
-
if len(desc) < 200:
|
|
481
|
-
score += 1
|
|
482
39
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (skill_dir / "scripts").exists():
|
|
486
|
-
score += 1
|
|
487
|
-
if lines <= 500:
|
|
488
|
-
score += 1
|
|
489
|
-
|
|
490
|
-
return min(score, 6)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
# === 100-POINT GRADING RUBRIC ===
|
|
494
|
-
#
|
|
495
|
-
# Ported from marketplace validator (scripts/validate-skills-schema.py)
|
|
496
|
-
# Grade Scale:
|
|
497
|
-
# A (90-100): Production-ready
|
|
498
|
-
# B (80-89): Good, minor improvements needed
|
|
499
|
-
# C (70-79): Adequate, has gaps
|
|
500
|
-
# D (60-69): Needs significant work
|
|
501
|
-
# F (<60): Major revision required
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
def score_progressive_disclosure(path: Path, body: str, fm: dict) -> dict:
|
|
505
|
-
"""Progressive Disclosure Architecture (30 pts max)."""
|
|
506
|
-
breakdown = {}
|
|
507
|
-
lines = len(body.splitlines())
|
|
508
|
-
skill_dir = path.parent
|
|
509
|
-
|
|
510
|
-
# Token Economy (10 pts)
|
|
511
|
-
if lines <= 150:
|
|
512
|
-
breakdown["token_economy"] = (10, f"Excellent: {lines} lines")
|
|
513
|
-
elif lines <= 300:
|
|
514
|
-
breakdown["token_economy"] = (7, f"Good: {lines} lines (target <=150)")
|
|
515
|
-
elif lines <= 500:
|
|
516
|
-
breakdown["token_economy"] = (4, f"Acceptable: {lines} lines (target <=150)")
|
|
517
|
-
else:
|
|
518
|
-
breakdown["token_economy"] = (0, f"Too long: {lines} lines (target <=150)")
|
|
519
|
-
|
|
520
|
-
# Layered Structure (10 pts)
|
|
521
|
-
refs_dir = skill_dir / "references"
|
|
522
|
-
if refs_dir.exists():
|
|
523
|
-
ref_files = list(refs_dir.glob("*.md"))
|
|
524
|
-
if ref_files:
|
|
525
|
-
breakdown["layered_structure"] = (10, f"Has references/ with {len(ref_files)} files")
|
|
526
|
-
else:
|
|
527
|
-
breakdown["layered_structure"] = (3, "references/ exists but empty")
|
|
528
|
-
else:
|
|
529
|
-
if lines <= 100:
|
|
530
|
-
breakdown["layered_structure"] = (8, "No references/ (acceptable for short skill)")
|
|
531
|
-
elif lines <= 200:
|
|
532
|
-
breakdown["layered_structure"] = (4, "No references/ (should extract content)")
|
|
533
|
-
else:
|
|
534
|
-
breakdown["layered_structure"] = (0, "No references/ (long skill needs extraction)")
|
|
535
|
-
|
|
536
|
-
# Reference Depth (5 pts)
|
|
537
|
-
if refs_dir.exists():
|
|
538
|
-
nested_dirs = [d for d in refs_dir.iterdir() if d.is_dir()]
|
|
539
|
-
if not nested_dirs:
|
|
540
|
-
breakdown["reference_depth"] = (5, "References are flat (good)")
|
|
541
|
-
else:
|
|
542
|
-
breakdown["reference_depth"] = (2, f"Nested dirs in references/: {len(nested_dirs)}")
|
|
543
|
-
else:
|
|
544
|
-
breakdown["reference_depth"] = (5, "N/A - no references/")
|
|
545
|
-
|
|
546
|
-
# Navigation Signals (5 pts)
|
|
547
|
-
has_toc = bool(re.search(r"(?mi)^##?\s*(table of contents|contents|toc)\b", body))
|
|
548
|
-
has_nav_links = bool(re.search(r"\[.*?\]\(#.*?\)", body))
|
|
549
|
-
if lines <= 100:
|
|
550
|
-
breakdown["navigation_signals"] = (5, "Short file, TOC optional")
|
|
551
|
-
elif has_toc or has_nav_links:
|
|
552
|
-
breakdown["navigation_signals"] = (5, "Has navigation/TOC")
|
|
553
|
-
else:
|
|
554
|
-
breakdown["navigation_signals"] = (0, "Long file needs TOC/navigation")
|
|
555
|
-
|
|
556
|
-
total = sum(v[0] for v in breakdown.values())
|
|
557
|
-
return {"score": total, "max": 30, "breakdown": breakdown}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def score_ease_of_use(path: Path, body: str, fm: dict) -> dict:
|
|
561
|
-
"""Ease of Use (25 pts max)."""
|
|
562
|
-
breakdown = {}
|
|
563
|
-
desc = str(fm.get("description", "")).lower()
|
|
564
|
-
|
|
565
|
-
# Metadata Quality (10 pts)
|
|
566
|
-
meta_score = 0
|
|
567
|
-
meta_notes = []
|
|
568
|
-
if fm.get("name"):
|
|
569
|
-
meta_score += 2
|
|
570
|
-
else:
|
|
571
|
-
meta_notes.append("missing name")
|
|
572
|
-
if fm.get("description") and len(str(fm.get("description", ""))) >= 50:
|
|
573
|
-
meta_score += 3
|
|
574
|
-
else:
|
|
575
|
-
meta_notes.append("description too short")
|
|
576
|
-
if fm.get("version"):
|
|
577
|
-
meta_score += 2
|
|
578
|
-
else:
|
|
579
|
-
meta_notes.append("missing version")
|
|
580
|
-
if fm.get("allowed-tools"):
|
|
581
|
-
meta_score += 2
|
|
582
|
-
else:
|
|
583
|
-
meta_notes.append("missing allowed-tools")
|
|
584
|
-
if fm.get("author") and "@" in str(fm.get("author", "")):
|
|
585
|
-
meta_score += 1
|
|
586
|
-
breakdown["metadata_quality"] = (meta_score, ", ".join(meta_notes) if meta_notes else "Complete metadata")
|
|
587
|
-
|
|
588
|
-
# Discoverability (6 pts)
|
|
589
|
-
disc_score = 0
|
|
590
|
-
disc_notes = []
|
|
591
|
-
if "use when" in desc:
|
|
592
|
-
disc_score += 3
|
|
593
|
-
disc_notes.append("has 'Use when'")
|
|
594
|
-
if "trigger with" in desc or "trigger phrase" in desc:
|
|
595
|
-
disc_score += 3
|
|
596
|
-
disc_notes.append("has trigger phrases")
|
|
597
|
-
if not disc_notes:
|
|
598
|
-
disc_notes.append("missing discovery cues")
|
|
599
|
-
breakdown["discoverability"] = (disc_score, ", ".join(disc_notes))
|
|
600
|
-
|
|
601
|
-
# Terminology Consistency (4 pts)
|
|
602
|
-
name = str(fm.get("name", ""))
|
|
603
|
-
folder = path.parent.name
|
|
604
|
-
term_score = 4
|
|
605
|
-
term_notes = []
|
|
606
|
-
if name and name != folder:
|
|
607
|
-
term_score -= 2
|
|
608
|
-
term_notes.append("name differs from folder")
|
|
609
|
-
if any(w.isupper() and len(w) > 3 for w in str(fm.get("description", "")).split()):
|
|
610
|
-
term_score -= 1
|
|
611
|
-
term_notes.append("inconsistent casing")
|
|
612
|
-
breakdown["terminology"] = (max(0, term_score), ", ".join(term_notes) if term_notes else "Consistent terminology")
|
|
613
|
-
|
|
614
|
-
# Workflow Clarity (5 pts)
|
|
615
|
-
workflow_score = 0
|
|
616
|
-
workflow_notes = []
|
|
617
|
-
if re.search(r"(?m)^\s*1\.\s+", body):
|
|
618
|
-
workflow_score += 3
|
|
619
|
-
workflow_notes.append("has numbered steps")
|
|
620
|
-
section_count = len(re.findall(r"(?m)^##\s+", body))
|
|
621
|
-
if section_count >= 5:
|
|
622
|
-
workflow_score += 2
|
|
623
|
-
workflow_notes.append(f"{section_count} sections")
|
|
624
|
-
elif section_count >= 3:
|
|
625
|
-
workflow_score += 1
|
|
626
|
-
workflow_notes.append(f"{section_count} sections (add more)")
|
|
627
|
-
if not workflow_notes:
|
|
628
|
-
workflow_notes.append("unclear workflow")
|
|
629
|
-
breakdown["workflow_clarity"] = (workflow_score, ", ".join(workflow_notes))
|
|
630
|
-
|
|
631
|
-
total = sum(v[0] for v in breakdown.values())
|
|
632
|
-
return {"score": total, "max": 25, "breakdown": breakdown}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
def score_utility(path: Path, body: str, fm: dict) -> dict:
|
|
636
|
-
"""Utility (20 pts max)."""
|
|
637
|
-
breakdown = {}
|
|
638
|
-
body_lower = body.lower()
|
|
639
|
-
|
|
640
|
-
# Problem Solving Power (8 pts)
|
|
641
|
-
problem_score = 0
|
|
642
|
-
problem_notes = []
|
|
643
|
-
if "## overview" in body_lower:
|
|
644
|
-
overview_match = re.search(r"## overview\s*\n(.*?)(?=\n##|\Z)", body, re.IGNORECASE | re.DOTALL)
|
|
645
|
-
if overview_match and len(overview_match.group(1).strip()) > 50:
|
|
646
|
-
problem_score += 4
|
|
647
|
-
problem_notes.append("has overview")
|
|
648
|
-
if "## prerequisites" in body_lower:
|
|
649
|
-
problem_score += 2
|
|
650
|
-
problem_notes.append("has prerequisites")
|
|
651
|
-
if "## output" in body_lower:
|
|
652
|
-
problem_score += 2
|
|
653
|
-
problem_notes.append("has output spec")
|
|
654
|
-
if not problem_notes:
|
|
655
|
-
problem_notes.append("unclear problem/solution")
|
|
656
|
-
breakdown["problem_solving"] = (problem_score, ", ".join(problem_notes))
|
|
657
|
-
|
|
658
|
-
# Degrees of Freedom (5 pts)
|
|
659
|
-
freedom_score = 0
|
|
660
|
-
freedom_notes = []
|
|
661
|
-
if re.search(r"(?i)(optional|configur|parameter|argument|flag|option)", body):
|
|
662
|
-
freedom_score += 2
|
|
663
|
-
freedom_notes.append("has options")
|
|
664
|
-
if re.search(r"(?i)(alternatively|or use|another approach|you can also)", body):
|
|
665
|
-
freedom_score += 2
|
|
666
|
-
freedom_notes.append("shows alternatives")
|
|
667
|
-
if re.search(r"(?i)(extend|customize|modify|adapt)", body):
|
|
668
|
-
freedom_score += 1
|
|
669
|
-
freedom_notes.append("extensible")
|
|
670
|
-
if not freedom_notes:
|
|
671
|
-
freedom_notes.append("rigid implementation")
|
|
672
|
-
breakdown["degrees_of_freedom"] = (freedom_score, ", ".join(freedom_notes))
|
|
673
|
-
|
|
674
|
-
# Feedback Loops (4 pts)
|
|
675
|
-
feedback_score = 0
|
|
676
|
-
feedback_notes = []
|
|
677
|
-
if "## error handling" in body_lower:
|
|
678
|
-
feedback_score += 2
|
|
679
|
-
feedback_notes.append("has error handling")
|
|
680
|
-
if re.search(r"(?i)(validate|verify|check|test|confirm)", body):
|
|
681
|
-
feedback_score += 1
|
|
682
|
-
feedback_notes.append("has validation")
|
|
683
|
-
if re.search(r"(?i)(troubleshoot|debug|diagnose|fix)", body):
|
|
684
|
-
feedback_score += 1
|
|
685
|
-
feedback_notes.append("has troubleshooting")
|
|
686
|
-
if not feedback_notes:
|
|
687
|
-
feedback_notes.append("no feedback mechanisms")
|
|
688
|
-
breakdown["feedback_loops"] = (feedback_score, ", ".join(feedback_notes))
|
|
689
|
-
|
|
690
|
-
# Examples & Templates (3 pts)
|
|
691
|
-
examples_score = 0
|
|
692
|
-
examples_notes = []
|
|
693
|
-
if "## examples" in body_lower or "**example" in body_lower:
|
|
694
|
-
examples_score += 2
|
|
695
|
-
examples_notes.append("has examples")
|
|
696
|
-
if "```" in body:
|
|
697
|
-
code_blocks = len(re.findall(r"```", body)) // 2
|
|
698
|
-
if code_blocks >= 2:
|
|
699
|
-
examples_score += 1
|
|
700
|
-
examples_notes.append(f"{code_blocks} code blocks")
|
|
701
|
-
if not examples_notes:
|
|
702
|
-
examples_notes.append("no examples")
|
|
703
|
-
breakdown["examples"] = (examples_score, ", ".join(examples_notes))
|
|
704
|
-
|
|
705
|
-
total = sum(v[0] for v in breakdown.values())
|
|
706
|
-
return {"score": total, "max": 20, "breakdown": breakdown}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def score_spec_compliance(path: Path, body: str, fm: dict) -> dict:
|
|
710
|
-
"""Spec Compliance (15 pts max)."""
|
|
711
|
-
breakdown = {}
|
|
712
|
-
name = str(fm.get("name", ""))
|
|
713
|
-
desc = str(fm.get("description", ""))
|
|
714
|
-
|
|
715
|
-
# Frontmatter Validity (5 pts)
|
|
716
|
-
fm_score = 5
|
|
717
|
-
fm_notes = []
|
|
718
|
-
required = {"name", "description", "allowed-tools", "version", "author", "license"}
|
|
719
|
-
missing = required - set(fm.keys())
|
|
720
|
-
if missing:
|
|
721
|
-
fm_score -= min(len(missing), 4)
|
|
722
|
-
fm_notes.append(f"missing: {', '.join(sorted(missing))}")
|
|
723
|
-
if not fm_notes:
|
|
724
|
-
fm_notes.append("valid frontmatter")
|
|
725
|
-
breakdown["frontmatter_validity"] = (max(0, fm_score), ", ".join(fm_notes))
|
|
726
|
-
|
|
727
|
-
# Name Conventions (4 pts)
|
|
728
|
-
name_score = 4
|
|
729
|
-
name_notes = []
|
|
730
|
-
if not re.match(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$", name) and len(name) > 1:
|
|
731
|
-
name_score -= 2
|
|
732
|
-
name_notes.append("not kebab-case")
|
|
733
|
-
if len(name) > 64:
|
|
734
|
-
name_score -= 1
|
|
735
|
-
name_notes.append("name too long")
|
|
736
|
-
if name != path.parent.name:
|
|
737
|
-
name_score -= 1
|
|
738
|
-
name_notes.append("name/folder mismatch")
|
|
739
|
-
if not name_notes:
|
|
740
|
-
name_notes.append("proper naming")
|
|
741
|
-
breakdown["name_conventions"] = (max(0, name_score), ", ".join(name_notes))
|
|
742
|
-
|
|
743
|
-
# Description Quality (4 pts)
|
|
744
|
-
desc_score = 4
|
|
745
|
-
desc_notes = []
|
|
746
|
-
if len(desc) < 50:
|
|
747
|
-
desc_score -= 2
|
|
748
|
-
desc_notes.append("too short")
|
|
749
|
-
if len(desc) > 1024:
|
|
750
|
-
desc_score -= 2
|
|
751
|
-
desc_notes.append("too long")
|
|
752
|
-
desc_lower = desc.lower()
|
|
753
|
-
if "i can" in desc_lower or "i will" in desc_lower:
|
|
754
|
-
desc_score -= 1
|
|
755
|
-
desc_notes.append("uses first person")
|
|
756
|
-
if "you can" in desc_lower or "you should" in desc_lower:
|
|
757
|
-
desc_score -= 1
|
|
758
|
-
desc_notes.append("uses second person")
|
|
759
|
-
if not desc_notes:
|
|
760
|
-
desc_notes.append("good description")
|
|
761
|
-
breakdown["description_quality"] = (max(0, desc_score), ", ".join(desc_notes))
|
|
762
|
-
|
|
763
|
-
# Optional Fields (2 pts)
|
|
764
|
-
opt_score = 2
|
|
765
|
-
opt_notes = []
|
|
766
|
-
if "model" in fm:
|
|
767
|
-
model = fm["model"]
|
|
768
|
-
if model not in ["inherit", "sonnet", "haiku", "opus"] and not str(model).startswith("claude-"):
|
|
769
|
-
opt_score -= 1
|
|
770
|
-
opt_notes.append("invalid model value")
|
|
771
|
-
if not opt_notes:
|
|
772
|
-
opt_notes.append("optional fields ok")
|
|
773
|
-
breakdown["optional_fields"] = (opt_score, ", ".join(opt_notes))
|
|
774
|
-
|
|
775
|
-
total = sum(v[0] for v in breakdown.values())
|
|
776
|
-
return {"score": total, "max": 15, "breakdown": breakdown}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
def score_writing_style(path: Path, body: str, fm: dict) -> dict:
|
|
780
|
-
"""Writing Style (10 pts max)."""
|
|
781
|
-
breakdown = {}
|
|
782
|
-
|
|
783
|
-
# Voice & Tense (4 pts)
|
|
784
|
-
voice_score = 4
|
|
785
|
-
voice_notes = []
|
|
786
|
-
imperative_verbs = ["create", "use", "run", "execute", "configure", "set", "add", "remove", "check", "verify"]
|
|
787
|
-
has_imperative = any(re.search(rf"(?m)^\s*\d+\.\s*{v}", body, re.IGNORECASE) for v in imperative_verbs)
|
|
788
|
-
if not has_imperative:
|
|
789
|
-
voice_score -= 2
|
|
790
|
-
voice_notes.append("use imperative voice")
|
|
791
|
-
if not voice_notes:
|
|
792
|
-
voice_notes.append("good voice")
|
|
793
|
-
breakdown["voice_tense"] = (voice_score, ", ".join(voice_notes))
|
|
794
|
-
|
|
795
|
-
# Objectivity (3 pts)
|
|
796
|
-
obj_score = 3
|
|
797
|
-
obj_notes = []
|
|
798
|
-
body_lower = body.lower()
|
|
799
|
-
if "you should" in body_lower or "you can" in body_lower or "you will" in body_lower:
|
|
800
|
-
obj_score -= 1
|
|
801
|
-
obj_notes.append("has second person")
|
|
802
|
-
if " i " in body_lower or "i can" in body_lower or "i'll" in body_lower:
|
|
803
|
-
obj_score -= 1
|
|
804
|
-
obj_notes.append("has first person")
|
|
805
|
-
if not obj_notes:
|
|
806
|
-
obj_notes.append("objective")
|
|
807
|
-
breakdown["objectivity"] = (max(0, obj_score), ", ".join(obj_notes))
|
|
808
|
-
|
|
809
|
-
# Conciseness (3 pts)
|
|
810
|
-
conc_score = 3
|
|
811
|
-
conc_notes = []
|
|
812
|
-
word_count = len(body.split())
|
|
813
|
-
lines = len(body.splitlines())
|
|
814
|
-
if word_count > 3000:
|
|
815
|
-
conc_score -= 2
|
|
816
|
-
conc_notes.append(f"verbose ({word_count} words)")
|
|
817
|
-
elif word_count > 2000:
|
|
818
|
-
conc_score -= 1
|
|
819
|
-
conc_notes.append(f"lengthy ({word_count} words)")
|
|
820
|
-
if lines > 400:
|
|
821
|
-
conc_score -= 1
|
|
822
|
-
conc_notes.append(f"many lines ({lines})")
|
|
823
|
-
if not conc_notes:
|
|
824
|
-
conc_notes.append("concise")
|
|
825
|
-
breakdown["conciseness"] = (max(0, conc_score), ", ".join(conc_notes))
|
|
826
|
-
|
|
827
|
-
total = sum(v[0] for v in breakdown.values())
|
|
828
|
-
return {"score": total, "max": 10, "breakdown": breakdown}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
def calculate_modifiers(path: Path, body: str, fm: dict) -> dict:
|
|
832
|
-
"""Modifiers (-5 to +5 pts)."""
|
|
833
|
-
modifiers = {}
|
|
834
|
-
name = str(fm.get("name", ""))
|
|
835
|
-
desc = str(fm.get("description", ""))
|
|
836
|
-
lines = len(body.splitlines())
|
|
837
|
-
|
|
838
|
-
# Bonuses
|
|
839
|
-
if name.endswith("ing") or any(name.endswith(f"-{s}") for s in ["ing"]):
|
|
840
|
-
modifiers["gerund_name"] = (+1, "gerund-style name")
|
|
841
|
-
|
|
842
|
-
sections = len(re.findall(r"(?m)^##\s+", body))
|
|
843
|
-
if sections >= 7:
|
|
844
|
-
modifiers["grep_friendly"] = (+1, "grep-friendly structure")
|
|
845
|
-
|
|
846
|
-
example_count = len(re.findall(r"(?i)\*\*example[:\s]", body))
|
|
847
|
-
if example_count >= 3:
|
|
848
|
-
modifiers["exemplary_examples"] = (+2, f"{example_count} labeled examples")
|
|
849
|
-
|
|
850
|
-
if "## resources" in body.lower():
|
|
851
|
-
external_links = len(re.findall(r"\[.*?\]\(https?://", body))
|
|
852
|
-
if external_links >= 2:
|
|
853
|
-
modifiers["external_resources"] = (+1, f"{external_links} external links")
|
|
854
|
-
|
|
855
|
-
# Penalties
|
|
856
|
-
desc_lower = desc.lower()
|
|
857
|
-
if "i can" in desc_lower or "i will" in desc_lower or "you can" in desc_lower or "you should" in desc_lower:
|
|
858
|
-
modifiers["person_in_desc"] = (-2, "first/second person in description")
|
|
859
|
-
|
|
860
|
-
has_toc = bool(re.search(r"(?mi)^##?\s*(table of contents|contents|toc)\b", body))
|
|
861
|
-
if lines > 150 and not has_toc:
|
|
862
|
-
modifiers["missing_toc"] = (-2, "long file needs TOC")
|
|
863
|
-
|
|
864
|
-
if "<" in body and ">" in body and re.search(r"<[a-z]+>", body):
|
|
865
|
-
modifiers["xml_tags"] = (-1, "XML-like tags in body")
|
|
866
|
-
|
|
867
|
-
total = sum(v[0] for v in modifiers.values())
|
|
868
|
-
total = max(-5, min(5, total))
|
|
869
|
-
return {"score": total, "max_bonus": 5, "max_penalty": -5, "items": modifiers}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
def grade_skill(path: Path, body: str, fm: dict) -> dict:
|
|
873
|
-
"""Calculate Intent Solutions 100-point grade."""
|
|
874
|
-
pda = score_progressive_disclosure(path, body, fm)
|
|
875
|
-
ease = score_ease_of_use(path, body, fm)
|
|
876
|
-
utility = score_utility(path, body, fm)
|
|
877
|
-
spec = score_spec_compliance(path, body, fm)
|
|
878
|
-
style = score_writing_style(path, body, fm)
|
|
879
|
-
mods = calculate_modifiers(path, body, fm)
|
|
880
|
-
|
|
881
|
-
base_score = pda["score"] + ease["score"] + utility["score"] + spec["score"] + style["score"]
|
|
882
|
-
total_score = max(0, min(100, base_score + mods["score"]))
|
|
883
|
-
|
|
884
|
-
if total_score >= 90:
|
|
885
|
-
grade = "A"
|
|
886
|
-
elif total_score >= 80:
|
|
887
|
-
grade = "B"
|
|
888
|
-
elif total_score >= 70:
|
|
889
|
-
grade = "C"
|
|
890
|
-
elif total_score >= 60:
|
|
891
|
-
grade = "D"
|
|
892
|
-
else:
|
|
893
|
-
grade = "F"
|
|
894
|
-
|
|
895
|
-
# Generate improvement suggestions
|
|
896
|
-
improvements = []
|
|
897
|
-
desc_lower = str(fm.get("description", "")).lower()
|
|
898
|
-
if "use when" not in desc_lower:
|
|
899
|
-
improvements.append('Add "Use when" to description (+3 pts)')
|
|
900
|
-
if "trigger with" not in desc_lower:
|
|
901
|
-
improvements.append('Add "Trigger with" to description (+3 pts)')
|
|
902
|
-
body_lower = body.lower()
|
|
903
|
-
if "## overview" not in body_lower:
|
|
904
|
-
improvements.append("Add ## Overview section (+4 pts)")
|
|
905
|
-
if "## prerequisites" not in body_lower:
|
|
906
|
-
improvements.append("Add ## Prerequisites section (+2 pts)")
|
|
907
|
-
if "## output" not in body_lower:
|
|
908
|
-
improvements.append("Add ## Output section (+2 pts)")
|
|
909
|
-
if "## error handling" not in body_lower:
|
|
910
|
-
improvements.append("Add ## Error Handling section (+2 pts)")
|
|
911
|
-
if "## examples" not in body_lower:
|
|
912
|
-
improvements.append("Add ## Examples section (+2 pts)")
|
|
913
|
-
if not fm.get("version"):
|
|
914
|
-
improvements.append("Add version field (+2 pts)")
|
|
915
|
-
if not fm.get("author"):
|
|
916
|
-
improvements.append("Add author field (+1 pt)")
|
|
917
|
-
if not fm.get("allowed-tools"):
|
|
918
|
-
improvements.append("Add allowed-tools field (+2 pts)")
|
|
919
|
-
|
|
920
|
-
return {
|
|
921
|
-
"score": total_score,
|
|
922
|
-
"grade": grade,
|
|
923
|
-
"improvements": improvements,
|
|
924
|
-
"breakdown": {
|
|
925
|
-
"progressive_disclosure": pda,
|
|
926
|
-
"ease_of_use": ease,
|
|
927
|
-
"utility": utility,
|
|
928
|
-
"spec_compliance": spec,
|
|
929
|
-
"writing_style": style,
|
|
930
|
-
"modifiers": mods,
|
|
931
|
-
},
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
def validate_skill(path: Path, enterprise: bool = False) -> Dict[str, Any]:
|
|
936
|
-
"""Validate a SKILL.md file."""
|
|
937
|
-
try:
|
|
938
|
-
content = path.read_text(encoding="utf-8")
|
|
939
|
-
except Exception as e:
|
|
940
|
-
return {"fatal": f"Cannot read file: {e}"}
|
|
941
|
-
|
|
942
|
-
try:
|
|
943
|
-
fm, body = parse_frontmatter(content)
|
|
944
|
-
except Exception as e:
|
|
945
|
-
return {"fatal": str(e)}
|
|
946
|
-
|
|
947
|
-
errors: List[str] = []
|
|
948
|
-
warnings: List[str] = []
|
|
949
|
-
info: List[str] = []
|
|
950
|
-
|
|
951
|
-
name_e, name_w = validate_name(fm, path, enterprise)
|
|
952
|
-
errors.extend(name_e)
|
|
953
|
-
warnings.extend(name_w)
|
|
954
|
-
|
|
955
|
-
desc_e, desc_w = validate_description(fm, enterprise)
|
|
956
|
-
errors.extend(desc_e)
|
|
957
|
-
warnings.extend(desc_w)
|
|
958
|
-
|
|
959
|
-
tools_e, tools_w = validate_tools(fm, enterprise)
|
|
960
|
-
errors.extend(tools_e)
|
|
961
|
-
warnings.extend(tools_w)
|
|
962
|
-
|
|
963
|
-
opt_e, opt_w = validate_optional_fields(fm)
|
|
964
|
-
errors.extend(opt_e)
|
|
965
|
-
warnings.extend(opt_w)
|
|
966
|
-
|
|
967
|
-
if enterprise:
|
|
968
|
-
meta_e, meta_w = validate_enterprise_metadata(fm)
|
|
969
|
-
errors.extend(meta_e)
|
|
970
|
-
warnings.extend(meta_w)
|
|
971
|
-
|
|
972
|
-
body_e, body_w, body_i = validate_body(body, path, enterprise)
|
|
973
|
-
errors.extend(body_e)
|
|
974
|
-
warnings.extend(body_w)
|
|
975
|
-
info.extend(body_i)
|
|
976
|
-
|
|
977
|
-
resource_errors = validate_resources(body, path)
|
|
978
|
-
errors.extend(resource_errors)
|
|
979
|
-
|
|
980
|
-
word_count = len(body.split())
|
|
981
|
-
line_count = len(body.splitlines())
|
|
982
|
-
token_estimate = int(word_count * 1.3)
|
|
983
|
-
disclosure_score = calculate_disclosure_score(fm, body, path)
|
|
984
|
-
|
|
985
|
-
return {
|
|
986
|
-
"valid": len(errors) == 0,
|
|
987
|
-
"tier": "Enterprise" if enterprise else "Standard",
|
|
988
|
-
"errors": errors,
|
|
989
|
-
"warnings": warnings,
|
|
990
|
-
"info": info,
|
|
991
|
-
"stats": {
|
|
992
|
-
"word_count": word_count,
|
|
993
|
-
"line_count": line_count,
|
|
994
|
-
"token_estimate": token_estimate,
|
|
995
|
-
"disclosure_score": disclosure_score,
|
|
996
|
-
},
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
def print_result(result: Dict[str, Any], path: Path) -> None:
|
|
1001
|
-
"""Print validation result in human-readable format."""
|
|
1002
|
-
if "fatal" in result:
|
|
1003
|
-
print(f"FATAL: {result['fatal']}")
|
|
1004
|
-
sys.exit(1)
|
|
1005
|
-
|
|
1006
|
-
tier = result["tier"]
|
|
1007
|
-
stats = result["stats"]
|
|
1008
|
-
|
|
1009
|
-
print(f"\n{'=' * 60}")
|
|
1010
|
-
print(f"SKILL VALIDATION: {path.name} [{tier} tier]")
|
|
1011
|
-
print(f"{'=' * 60}\n")
|
|
1012
|
-
|
|
1013
|
-
if result["errors"]:
|
|
1014
|
-
print("ERRORS:")
|
|
1015
|
-
for e in result["errors"]:
|
|
1016
|
-
print(f" x {e}")
|
|
1017
|
-
print()
|
|
1018
|
-
|
|
1019
|
-
if result["warnings"]:
|
|
1020
|
-
print("WARNINGS:")
|
|
1021
|
-
for w in result["warnings"]:
|
|
1022
|
-
print(f" ! {w}")
|
|
1023
|
-
print()
|
|
1024
|
-
|
|
1025
|
-
if result["info"]:
|
|
1026
|
-
print("INFO:")
|
|
1027
|
-
for i in result["info"]:
|
|
1028
|
-
print(f" - {i}")
|
|
1029
|
-
print()
|
|
1030
|
-
|
|
1031
|
-
print("Stats:")
|
|
1032
|
-
print(f" Words: {stats['word_count']}")
|
|
1033
|
-
print(f" Lines: {stats['line_count']}")
|
|
1034
|
-
print(f" Tokens (est.): {stats['token_estimate']}")
|
|
1035
|
-
print(f" Disclosure Score: {stats['disclosure_score']}/6")
|
|
1036
|
-
print()
|
|
1037
|
-
|
|
1038
|
-
if result["valid"]:
|
|
1039
|
-
print("PASSED")
|
|
1040
|
-
else:
|
|
1041
|
-
print(f"FAILED ({len(result['errors'])} errors)")
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
def print_grade(grade_result: Dict[str, Any], path: Path) -> None:
|
|
1045
|
-
"""Print 100-point grade report."""
|
|
1046
|
-
bd = grade_result["breakdown"]
|
|
1047
|
-
|
|
1048
|
-
print(f"\n{'=' * 60}")
|
|
1049
|
-
print(f"SKILL GRADE: {path.name}")
|
|
1050
|
-
print(f"{'=' * 60}\n")
|
|
1051
|
-
|
|
1052
|
-
print(f"Grade: {grade_result['grade']} ({grade_result['score']}/100)\n")
|
|
1053
|
-
|
|
1054
|
-
# Print each pillar
|
|
1055
|
-
pillars = [
|
|
1056
|
-
("Progressive Disclosure", "progressive_disclosure"),
|
|
1057
|
-
("Ease of Use", "ease_of_use"),
|
|
1058
|
-
("Utility", "utility"),
|
|
1059
|
-
("Spec Compliance", "spec_compliance"),
|
|
1060
|
-
("Writing Style", "writing_style"),
|
|
1061
|
-
]
|
|
1062
|
-
|
|
1063
|
-
for label, key in pillars:
|
|
1064
|
-
pillar = bd[key]
|
|
1065
|
-
sub_scores = ", ".join(f"{k}={v[0]}" for k, v in pillar["breakdown"].items())
|
|
1066
|
-
print(f" {label + ':':28s} {pillar['score']:2d}/{pillar['max']} [{sub_scores}]")
|
|
1067
|
-
|
|
1068
|
-
# Modifiers
|
|
1069
|
-
mods = bd["modifiers"]
|
|
1070
|
-
mod_items = ", ".join(f"{k}={v[0]:+d}" for k, v in mods["items"].items()) if mods["items"] else "none"
|
|
1071
|
-
sign = "+" if mods["score"] >= 0 else ""
|
|
1072
|
-
print(f" {'Modifiers:':28s} {sign}{mods['score']} [{mod_items}]")
|
|
1073
|
-
print()
|
|
1074
|
-
|
|
1075
|
-
# Improvements
|
|
1076
|
-
if grade_result["improvements"]:
|
|
1077
|
-
print("Improvements:")
|
|
1078
|
-
for imp in grade_result["improvements"]:
|
|
1079
|
-
print(f" - {imp}")
|
|
1080
|
-
print()
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
def main():
|
|
1084
|
-
parser = argparse.ArgumentParser(description="Validate SKILL.md files (Standard tier by default)")
|
|
40
|
+
def main() -> int:
|
|
41
|
+
parser = argparse.ArgumentParser(description="Validate SKILL.md files (delegates to validate-skills-schema.py)")
|
|
1085
42
|
parser.add_argument("path", help="Path to SKILL.md file")
|
|
1086
43
|
parser.add_argument(
|
|
1087
44
|
"--standard",
|
|
@@ -1091,65 +48,46 @@ def main():
|
|
|
1091
48
|
parser.add_argument(
|
|
1092
49
|
"--enterprise",
|
|
1093
50
|
action="store_true",
|
|
1094
|
-
help="Use Enterprise tier
|
|
51
|
+
help="Use Enterprise tier (maps to the main validator's --marketplace tier)",
|
|
1095
52
|
)
|
|
1096
|
-
parser.add_argument("--grade", action="store_true", help="Run 100-point grading rubric")
|
|
53
|
+
parser.add_argument("--grade", action="store_true", help="Run 100-point grading rubric (marketplace tier)")
|
|
1097
54
|
parser.add_argument("--json", action="store_true", help="JSON output")
|
|
1098
55
|
args = parser.parse_args()
|
|
1099
56
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
sys.
|
|
1104
|
-
|
|
1105
|
-
enterprise = args.enterprise and not args.standard
|
|
1106
|
-
|
|
1107
|
-
if args.grade:
|
|
1108
|
-
try:
|
|
1109
|
-
content = path.read_text(encoding="utf-8")
|
|
1110
|
-
fm, body = parse_frontmatter(content)
|
|
1111
|
-
except Exception as e:
|
|
1112
|
-
print(f"FATAL: {e}", file=sys.stderr)
|
|
1113
|
-
sys.exit(1)
|
|
1114
|
-
|
|
1115
|
-
grade_result = grade_skill(path, body, fm)
|
|
1116
|
-
|
|
1117
|
-
if args.json:
|
|
1118
|
-
# Serialize breakdown for JSON (convert tuples to dicts)
|
|
1119
|
-
output = {
|
|
1120
|
-
"score": grade_result["score"],
|
|
1121
|
-
"grade": grade_result["grade"],
|
|
1122
|
-
"improvements": grade_result["improvements"],
|
|
1123
|
-
"breakdown": {},
|
|
1124
|
-
}
|
|
1125
|
-
for pillar_key, pillar_data in grade_result["breakdown"].items():
|
|
1126
|
-
if pillar_key == "modifiers":
|
|
1127
|
-
output["breakdown"]["modifiers"] = {
|
|
1128
|
-
"score": pillar_data["score"],
|
|
1129
|
-
"items": {k: {"points": v[0], "reason": v[1]} for k, v in pillar_data["items"].items()},
|
|
1130
|
-
}
|
|
1131
|
-
else:
|
|
1132
|
-
output["breakdown"][pillar_key] = {
|
|
1133
|
-
"score": pillar_data["score"],
|
|
1134
|
-
"max": pillar_data["max"],
|
|
1135
|
-
"breakdown": {k: {"points": v[0], "reason": v[1]} for k, v in pillar_data["breakdown"].items()},
|
|
1136
|
-
}
|
|
1137
|
-
print(json.dumps(output, indent=2))
|
|
1138
|
-
else:
|
|
1139
|
-
print_grade(grade_result, path)
|
|
1140
|
-
|
|
1141
|
-
sys.exit(0 if grade_result["score"] >= 60 else 1)
|
|
57
|
+
print(
|
|
58
|
+
"DEPRECATED: this wrapper delegates to scripts/validate-skills-schema.py "
|
|
59
|
+
"(the authoritative validator); invoke it directly for the full flag surface.",
|
|
60
|
+
file=sys.stderr,
|
|
61
|
+
)
|
|
1142
62
|
|
|
1143
|
-
|
|
1144
|
-
|
|
63
|
+
target = Path(args.path)
|
|
64
|
+
if not target.exists():
|
|
65
|
+
print(f"Error: {target} not found", file=sys.stderr)
|
|
66
|
+
return 1
|
|
1145
67
|
|
|
1146
|
-
|
|
1147
|
-
|
|
68
|
+
validator = find_main_validator()
|
|
69
|
+
if validator is None:
|
|
70
|
+
print(
|
|
71
|
+
"ERROR: Cannot find scripts/validate-skills-schema.py — run from within the claude-code-plugins repo",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
cmd = [sys.executable, str(validator)]
|
|
77
|
+
# Tier mapping (at most one tier flag; main validator rejects combinations):
|
|
78
|
+
# --grade or --enterprise -> --marketplace (rubric grading lives there)
|
|
79
|
+
# --standard (or default) -> --standard
|
|
80
|
+
# Previous behavior: --standard overrode --enterprise; --grade always graded.
|
|
81
|
+
if args.grade or (args.enterprise and not args.standard):
|
|
82
|
+
cmd.append("--marketplace")
|
|
1148
83
|
else:
|
|
1149
|
-
|
|
84
|
+
cmd.append("--standard")
|
|
85
|
+
if args.json:
|
|
86
|
+
cmd.append("--json")
|
|
87
|
+
cmd.append(str(target))
|
|
1150
88
|
|
|
1151
|
-
|
|
89
|
+
return subprocess.run(cmd).returncode
|
|
1152
90
|
|
|
1153
91
|
|
|
1154
92
|
if __name__ == "__main__":
|
|
1155
|
-
main()
|
|
93
|
+
sys.exit(main())
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: analyzer
|
|
3
3
|
description: Analyze blind comparison results to understand why the winner won and generate improvement suggestions
|
|
4
|
+
tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Bash
|
|
9
|
+
- Glob
|
|
10
|
+
- Grep
|
|
11
|
+
- WebFetch
|
|
12
|
+
- WebSearch
|
|
13
|
+
- Task
|
|
14
|
+
- TodoWrite
|
|
15
|
+
model: sonnet
|
|
16
|
+
color: orange
|
|
17
|
+
version: 1.0.0
|
|
18
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
19
|
+
tags:
|
|
20
|
+
- skill-enhancers
|
|
21
|
+
- analyzer
|
|
22
|
+
disallowedTools: []
|
|
23
|
+
skills: []
|
|
24
|
+
background: false
|
|
25
|
+
# ── upgrade levers — uncomment + set when tuning this agent ──
|
|
26
|
+
# effort: high # reasoning depth: low/medium/high/xhigh/max (omit = inherit session)
|
|
27
|
+
# maxTurns: 50 # cap the agentic loop (omit = engine default)
|
|
28
|
+
# memory: project # persistent scope: user/project/local (omit = ephemeral)
|
|
29
|
+
# isolation: worktree # run in an isolated git worktree
|
|
30
|
+
# initialPrompt: "…" # seed the agent's first turn
|
|
31
|
+
# hooks / mcpServers / permissionMode → set at the PLUGIN level, not on a plugin agent
|
|
4
32
|
---
|
|
5
|
-
|
|
6
33
|
# Post-hoc Analyzer Agent
|
|
7
34
|
|
|
8
35
|
Analyze blind comparison results to understand WHY the winner won and generate improvement suggestions.
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: comparator
|
|
3
3
|
description: Compare two outputs blindly without knowing which skill produced them
|
|
4
|
+
tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Bash
|
|
9
|
+
- Glob
|
|
10
|
+
- Grep
|
|
11
|
+
- WebFetch
|
|
12
|
+
- WebSearch
|
|
13
|
+
- Task
|
|
14
|
+
- TodoWrite
|
|
15
|
+
model: sonnet
|
|
16
|
+
color: red
|
|
17
|
+
version: 1.0.0
|
|
18
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
19
|
+
tags:
|
|
20
|
+
- skill-enhancers
|
|
21
|
+
- comparator
|
|
22
|
+
disallowedTools: []
|
|
23
|
+
skills: []
|
|
24
|
+
background: false
|
|
25
|
+
# ── upgrade levers — uncomment + set when tuning this agent ──
|
|
26
|
+
# effort: high # reasoning depth: low/medium/high/xhigh/max (omit = inherit session)
|
|
27
|
+
# maxTurns: 50 # cap the agentic loop (omit = engine default)
|
|
28
|
+
# memory: project # persistent scope: user/project/local (omit = ephemeral)
|
|
29
|
+
# isolation: worktree # run in an isolated git worktree
|
|
30
|
+
# initialPrompt: "…" # seed the agent's first turn
|
|
31
|
+
# hooks / mcpServers / permissionMode → set at the PLUGIN level, not on a plugin agent
|
|
4
32
|
---
|
|
5
|
-
|
|
6
33
|
# Blind Comparator Agent
|
|
7
34
|
|
|
8
35
|
Compare two outputs WITHOUT knowing which skill produced them.
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grader
|
|
3
3
|
description: Evaluate expectations against execution transcripts and outputs
|
|
4
|
+
tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Bash
|
|
9
|
+
- Glob
|
|
10
|
+
- Grep
|
|
11
|
+
- WebFetch
|
|
12
|
+
- WebSearch
|
|
13
|
+
- Task
|
|
14
|
+
- TodoWrite
|
|
15
|
+
model: sonnet
|
|
16
|
+
color: pink
|
|
17
|
+
version: 1.0.0
|
|
18
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
19
|
+
tags:
|
|
20
|
+
- skill-enhancers
|
|
21
|
+
- grader
|
|
22
|
+
disallowedTools: []
|
|
23
|
+
skills: []
|
|
24
|
+
background: false
|
|
25
|
+
# ── upgrade levers — uncomment + set when tuning this agent ──
|
|
26
|
+
# effort: high # reasoning depth: low/medium/high/xhigh/max (omit = inherit session)
|
|
27
|
+
# maxTurns: 50 # cap the agentic loop (omit = engine default)
|
|
28
|
+
# memory: project # persistent scope: user/project/local (omit = ephemeral)
|
|
29
|
+
# isolation: worktree # run in an isolated git worktree
|
|
30
|
+
# initialPrompt: "…" # seed the agent's first turn
|
|
31
|
+
# hooks / mcpServers / permissionMode → set at the PLUGIN level, not on a plugin agent
|
|
4
32
|
---
|
|
5
|
-
|
|
6
33
|
# Grader Agent
|
|
7
34
|
|
|
8
35
|
Evaluate expectations against an execution transcript and outputs.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|