@ssheleg/make-skill 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/README.md +12 -5
- package/SECURITY.md +10 -4
- package/bin/make-skill.js +18 -0
- package/cursor/rules/make-skill.mdc +20 -6
- package/package.json +2 -2
- package/plugins/make-skill/.claude-plugin/plugin.json +1 -1
- package/plugins/make-skill/agents/skill-auditor.md +9 -9
- package/plugins/make-skill/bin/make-skill-audit +41 -0
- package/plugins/make-skill/commands/skill-audit.md +10 -11
- package/plugins/make-skill/hooks/hooks.json +1 -1
- package/plugins/make-skill/skills/make-skill/SKILL.md +136 -155
- package/plugins/make-skill/skills/make-skill/assets/agent.template.md +8 -6
- package/plugins/make-skill/skills/make-skill/assets/command.template.md +7 -6
- package/plugins/make-skill/skills/make-skill/references/agent-skills-spec.md +3 -1
- package/plugins/make-skill/skills/make-skill/references/claude-code-plugin.md +9 -0
- package/plugins/make-skill/skills/make-skill/references/distribution.md +7 -1
- package/plugins/make-skill/skills/make-skill/references/host-capabilities.md +35 -9
- package/plugins/make-skill/skills/make-skill/references/mcp-ship.md +8 -8
- package/plugins/make-skill/skills/make-skill/references/retrofit.md +40 -13
- package/plugins/make-skill/skills/make-skill/scripts/audit_skill.py +43 -20
|
@@ -19,14 +19,14 @@ schema is dated by design.
|
|
|
19
19
|
|
|
20
20
|
## Contents
|
|
21
21
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
22
|
+
- Mounting into an existing web app
|
|
23
|
+
- Auth middleware and a health endpoint
|
|
24
|
+
- Client configuration
|
|
25
|
+
- Debugging a client that will not connect
|
|
26
|
+
- Publishing to the MCP Registry
|
|
27
|
+
- Registry name formats and who may claim them
|
|
28
|
+
- Automating publication
|
|
29
|
+
- Versioning
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
@@ -14,18 +14,33 @@ standard — including the no-argument `/make-skill` path, where a `SKILL.md`,
|
|
|
14
14
|
|
|
15
15
|
## Run the script first
|
|
16
16
|
|
|
17
|
-
The mechanical half is deterministic — do not reason your way through it
|
|
17
|
+
The mechanical half is deterministic — do not reason your way through it. In
|
|
18
|
+
Claude Code the plugin's `bin/` is on the Bash tool's PATH, so the auditor is
|
|
19
|
+
reachable by name:
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
|
|
22
|
+
make-skill-audit <skill-dir> --house
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
On any other host, run the script it wraps — `scripts/audit_skill.py` inside the
|
|
26
|
+
make-skill directory you are reading this from (`~/.agents/skills/make-skill/`,
|
|
27
|
+
`~/.claude/skills/make-skill/`):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 <make-skill dir>/scripts/audit_skill.py <skill-dir> --house
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Do not build the command out of `${CLAUDE_PLUGIN_ROOT}`.** That variable is
|
|
34
|
+
substituted into skill, command and agent *text*, but it is **not exported to
|
|
35
|
+
the Bash tool's environment** — a command written around it expands to
|
|
36
|
+
`/skills/...` and dies with "No such file or directory" (measured, Claude Code
|
|
37
|
+
2.1.220). That failure is how a mechanical check turns into a reasoned-about
|
|
38
|
+
PASS, which is the one outcome this file exists to prevent.
|
|
39
|
+
|
|
40
|
+
No `python3` at all? Record items 1 and 9 as **NOT-RUN** with that reason and
|
|
41
|
+
check them by hand against `references/agent-skills-spec.md` — the script
|
|
42
|
+
implements that checklist and nothing more. Either way, the judgement items are
|
|
43
|
+
yours.
|
|
29
44
|
|
|
30
45
|
## How to report
|
|
31
46
|
|
|
@@ -35,11 +50,21 @@ Audit first, fix second, in the same session. One verdict per item:
|
|
|
35
50
|
|---|---|
|
|
36
51
|
| **PASS** | the check was actually run — a command's output or a `file:line` |
|
|
37
52
|
| **GAP** | what is wrong, the evidence, and the fix that closes it |
|
|
53
|
+
| **NOT-RUN** | the tool the check needs is absent — name the tool and the reason |
|
|
38
54
|
|
|
39
55
|
"Looks fine" is not a verdict. Neither is a PASS on a check that was reasoned
|
|
40
56
|
about rather than executed: `python3 test/validate.py`, `claude plugin validate
|
|
41
57
|
… --strict`, `npx skills add <repo> --list` all produce output, and the output is
|
|
42
|
-
the evidence.
|
|
58
|
+
the evidence.
|
|
59
|
+
|
|
60
|
+
**NOT-RUN exists so that "I could not run this" has somewhere to go.** A
|
|
61
|
+
checklist with only PASS and GAP forces an unrunnable check into one of them,
|
|
62
|
+
and the cheaper lie is PASS. Item 1's `skills-ref validate` is the standing
|
|
63
|
+
example: it installs from source only, so on most machines it is NOT-RUN, and
|
|
64
|
+
saying so is the honest result. A NOT-RUN you did not try to resolve is still a
|
|
65
|
+
gap in the audit — say what would make it runnable.
|
|
66
|
+
|
|
67
|
+
Report the table before changing anything, then fix.
|
|
43
68
|
|
|
44
69
|
## The 14-item audit checklist
|
|
45
70
|
|
|
@@ -84,15 +109,17 @@ the evidence. Report the gap table before changing anything, then fix.
|
|
|
84
109
|
skills add <repo> --list` lists ONLY real skills; `npx <name>` works from a
|
|
85
110
|
non-repo cwd if it is published; `.mdc` rules valid and free of relative
|
|
86
111
|
links.
|
|
87
|
-
11. **Repo meta**: homepage, description and topics set on the forge
|
|
112
|
+
11. **Repo meta**: homepage, description and topics set on the forge (any
|
|
113
|
+
consistent homepage convention — do not "fix" a deliberate one); LICENSE
|
|
88
114
|
present and declared in the front matter and the marketplace entry;
|
|
89
115
|
CHANGELOG current; public repos also carry CONTRIBUTING.md and SECURITY.md.
|
|
90
116
|
12. **Gotcha compliance**: the list in `SKILL.md`, plus the installer traps in
|
|
91
117
|
`references/distribution.md` when the repo ships a CLI or a validator.
|
|
92
118
|
13. **Protocol dependencies** if it touches MCP or A2A (`references/mcp.md`,
|
|
93
|
-
`references/
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
`references/mcp-ship.md` once a server is being shipped, `references/a2a.md`):
|
|
120
|
+
dependency and protocol version in `compatibility`, discovery instead of
|
|
121
|
+
hardcoded tool names, the untrusted-output rule stated, interactive auth
|
|
122
|
+
handled as a human step rather than a retry loop.
|
|
96
123
|
14. **Host capabilities and their fallbacks**
|
|
97
124
|
(`references/host-capabilities.md`) if it ships a hook, subagent, command or
|
|
98
125
|
MCP server: the degradation contract written in the body for all three axes
|
|
@@ -171,7 +171,10 @@ def _check_name(a, fm, lines, dir_name, rel):
|
|
|
171
171
|
a.gap("NAME_CHARSET", "name %r must be lowercase a-z0-9 with single internal "
|
|
172
172
|
"hyphens — no uppercase, no leading/trailing or doubled hyphen" % name, rel, ln)
|
|
173
173
|
else:
|
|
174
|
-
|
|
174
|
+
# Charset only. Saying "spec-legal" here would contradict NAME_RESERVED
|
|
175
|
+
# and NAME_XML below, and a PASS line quoted out of context is exactly
|
|
176
|
+
# how a wrong verdict acquires real command output as its evidence.
|
|
177
|
+
a.ok("NAME_CHARSET", "name %r uses a legal charset" % name, rel, ln)
|
|
175
178
|
if name != dir_name:
|
|
176
179
|
a.gap("NAME_DIR", "name %r != directory %r — the spec requires them to match, "
|
|
177
180
|
"and the Skills API matches the uploaded top-level directory against it"
|
|
@@ -211,10 +214,16 @@ def _check_description(a, fm, lines, rel, house):
|
|
|
211
214
|
else:
|
|
212
215
|
a.ok("DESC_PERSON", "description is third person", rel, ln)
|
|
213
216
|
if house:
|
|
217
|
+
# Both report either way: "the house rules were checked" has to be
|
|
218
|
+
# provable from the output the canon tells the agent to cite.
|
|
214
219
|
if not desc.lower().startswith("use when"):
|
|
215
220
|
a.gap("DESC_USEWHEN", "description must start with 'Use when …' (house rule)", rel, ln)
|
|
221
|
+
else:
|
|
222
|
+
a.ok("DESC_USEWHEN", "description opens with 'Use when …' (house rule)", rel, ln)
|
|
216
223
|
if not re.search(r"[а-яё]", desc, re.I):
|
|
217
224
|
a.gap("DESC_RU", "description carries no Russian trigger phrases (house rule)", rel, ln)
|
|
225
|
+
else:
|
|
226
|
+
a.ok("DESC_RU", "description carries Russian trigger phrases (house rule)", rel, ln)
|
|
218
227
|
|
|
219
228
|
|
|
220
229
|
def _check_optional_fields(a, fm, lines, rel):
|
|
@@ -253,13 +262,18 @@ def _check_keys(a, fm, lines, rel):
|
|
|
253
262
|
def _check_body_budget(a, body, rel):
|
|
254
263
|
n_lines = body.count("\n") + 1
|
|
255
264
|
est = int(len(body) / CHARS_PER_TOKEN)
|
|
265
|
+
# Both, not either: a body over the line budget still has to report its
|
|
266
|
+
# token count, or the second fix arrives only after the first one ships.
|
|
267
|
+
over = False
|
|
256
268
|
if n_lines >= BODY_MAX_LINES:
|
|
257
269
|
a.gap("BODY_LINES", "body is %d lines, the budget is < %d — move detail into "
|
|
258
270
|
"references/" % (n_lines, BODY_MAX_LINES), rel)
|
|
259
|
-
|
|
271
|
+
over = True
|
|
272
|
+
if est >= BODY_MAX_TOKENS:
|
|
260
273
|
a.gap("BODY_TOKENS", "body is ~%d tokens (%d chars / %s), the budget is < %d"
|
|
261
274
|
% (est, len(body), CHARS_PER_TOKEN, BODY_MAX_TOKENS), rel)
|
|
262
|
-
|
|
275
|
+
over = True
|
|
276
|
+
if not over:
|
|
263
277
|
a.ok("BODY_BUDGET", "body is %d lines / ~%d tokens (budget %d / %d)"
|
|
264
278
|
% (n_lines, est, BODY_MAX_LINES, BODY_MAX_TOKENS), rel)
|
|
265
279
|
|
|
@@ -295,21 +309,27 @@ def _check_bundle(a, skill_dir, skill_text, dir_name):
|
|
|
295
309
|
|
|
296
310
|
|
|
297
311
|
def _check_links(a, skill_dir, skill_text, rel):
|
|
298
|
-
"""A relative link that escapes the skill directory arrives broken everywhere.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
"""A relative link that escapes the skill directory arrives broken everywhere.
|
|
313
|
+
|
|
314
|
+
Reported per line: the canon defines evidence as a `file:line`, so a finding
|
|
315
|
+
without one forces the agent to either drop the evidence or invent it.
|
|
316
|
+
"""
|
|
317
|
+
bad = False
|
|
318
|
+
for i, line in enumerate(skill_text.split("\n"), start=1):
|
|
319
|
+
for target in LINK_RE.findall(line):
|
|
320
|
+
if target.startswith(("http://", "https://", "mailto:", "#")):
|
|
321
|
+
continue
|
|
322
|
+
if target.startswith("../") or "/../" in target:
|
|
323
|
+
a.gap("LINK_ESCAPE", "link %r escapes the skill directory — packagers "
|
|
324
|
+
"ship that directory alone, so it arrives broken on every agent"
|
|
325
|
+
% target, rel, i)
|
|
326
|
+
bad = True
|
|
327
|
+
continue
|
|
328
|
+
path = os.path.normpath(os.path.join(skill_dir, target.split("#")[0]))
|
|
329
|
+
if not os.path.exists(path):
|
|
330
|
+
a.gap("LINK_BROKEN", "link %r does not resolve" % target, rel, i)
|
|
331
|
+
bad = True
|
|
332
|
+
if not bad:
|
|
313
333
|
a.ok("LINK_INTEGRITY", "every relative link resolves and stays inside the skill", rel)
|
|
314
334
|
|
|
315
335
|
|
|
@@ -335,8 +355,11 @@ def _check_prose(a, body, rel):
|
|
|
335
355
|
a.gap("TIME_BRANCH", "time-branching instruction (%r) — it is wrong the day "
|
|
336
356
|
"it ships; put superseded material under '## Old patterns'"
|
|
337
357
|
% raw.strip()[:60], rel, i)
|
|
338
|
-
#
|
|
339
|
-
|
|
358
|
+
# A pointer at a directory rather than a file with a load condition. Quoted
|
|
359
|
+
# spans are blanked here too: a canon that forbids "see references/" has to
|
|
360
|
+
# be able to quote the phrase it forbids.
|
|
361
|
+
for i, raw in enumerate(body.split("\n"), start=1):
|
|
362
|
+
line = _strip_quoted(raw)
|
|
340
363
|
if re.search(r"\b(?:see|read)\s+`?(?:references|scripts|assets)/`?(?![\w.-])", line, re.I):
|
|
341
364
|
a.gap("REF_NO_TRIGGER", "points at a directory instead of a file with a "
|
|
342
365
|
"stated load condition — 'read X when Y' beats 'see references/'", rel, i)
|