@ssheleg/make-skill 0.10.0 → 0.11.1
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 +180 -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 +139 -157
- 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 +55 -20
|
@@ -40,6 +40,7 @@ but did not write is not a fallback.
|
|
|
40
40
|
| Capability | Buys you | Costs | Reach for it when |
|
|
41
41
|
|---|---|---|---|
|
|
42
42
|
| `scripts/` | determinism, and output instead of code in context | none until run | the agent re-derives the same logic every run |
|
|
43
|
+
| `bin/` | a bundled script reachable **by name** — Claude Code puts it on the Bash tool's PATH | none until run | a documented command must work without a path variable (see *Scripts*) |
|
|
43
44
|
| `hooks/` | enforcement at the moment of the action | runs on the user's machine, every matching tool call | a rule matters more than an instruction can guarantee |
|
|
44
45
|
| `agents/` | a job done in a separate context window | ~100 always-on tokens per agent | the work would otherwise crowd out the main thread |
|
|
45
46
|
| `commands/` | a user-typed entry point with arguments | ~50–100 always-on tokens | a human, not the model, decides when it runs |
|
|
@@ -154,19 +155,42 @@ carry the trigger phrases that reach the same behavior in plain language.
|
|
|
154
155
|
|
|
155
156
|
The one accelerator that travels. Keep it inside the skill directory
|
|
156
157
|
(`scripts/`), stdlib-only, and invoke it by a path the agent can actually
|
|
157
|
-
resolve
|
|
158
|
+
resolve.
|
|
159
|
+
|
|
160
|
+
**The trap that costs the most here: `${CLAUDE_PLUGIN_ROOT}` does not work in a
|
|
161
|
+
command you tell the agent to run.** It is substituted into skill, command and
|
|
162
|
+
agent *text*, and it is exported to *hook and monitor* processes — but it is
|
|
163
|
+
**not** in the Bash tool's environment. Measured on Claude Code 2.1.220:
|
|
158
164
|
|
|
159
165
|
```bash
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
python3
|
|
166
|
+
$ echo "[${CLAUDE_PLUGIN_ROOT}]"
|
|
167
|
+
[]
|
|
168
|
+
$ python3 "${CLAUDE_PLUGIN_ROOT}/skills/x/scripts/x.py" .
|
|
169
|
+
python3: can't open file '/skills/x/scripts/x.py': [Errno 2] No such file or directory
|
|
163
170
|
```
|
|
164
171
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
That failure is expensive out of proportion to its size: the agent was told to
|
|
173
|
+
run a deterministic check, the check will not run, and reasoning through it
|
|
174
|
+
instead — then reporting the result as if it had run — is the cheapest way out.
|
|
175
|
+
|
|
176
|
+
**Ship a wrapper in the plugin's `bin/` instead.** Claude Code puts a plugin's
|
|
177
|
+
`bin/` on the Bash tool's PATH while the plugin is enabled, so the script
|
|
178
|
+
becomes reachable by name, with no variable in the command:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
<plugin>-<verb> <target> # Claude Code: bin/ is on PATH
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Make the wrapper resolve its payload from **its own location** (`dirname "$0"`,
|
|
185
|
+
following symlinks), never from an environment variable — then it also works
|
|
186
|
+
copied, symlinked into an agents hub, or called by absolute path. Worked
|
|
187
|
+
example: this plugin's `bin/make-skill-audit`.
|
|
188
|
+
|
|
189
|
+
Everywhere else the absolute path differs per channel
|
|
190
|
+
(`~/.agents/skills/<skill>/`, `~/.claude/skills/<skill>/`), so the instruction
|
|
191
|
+
that survives is "run the script from the skill directory you just read this
|
|
192
|
+
file from". State the no-interpreter fallback too: the manual procedure, in the
|
|
193
|
+
reference that the script implements, and the items it leaves **NOT-RUN**.
|
|
170
194
|
|
|
171
195
|
## MCP servers and other plugins as dependencies
|
|
172
196
|
|
|
@@ -215,4 +239,6 @@ what the agent reads at the exact moment something is missing.
|
|
|
215
239
|
- [ ] No command named after a skill; every `argument-hint` quoted
|
|
216
240
|
- [ ] Plugin agents carry no `hooks` / `mcpServers` / `permissionMode`
|
|
217
241
|
- [ ] Scripts are stdlib-only, inside the skill dir, invoked by a resolvable path
|
|
242
|
+
- [ ] No command the agent is told to RUN contains `${CLAUDE_PLUGIN_ROOT}` — it is
|
|
243
|
+
empty in the Bash tool; ship a `bin/` wrapper and call it by name
|
|
218
244
|
- [ ] MCP and sibling-skill dependencies declared in `compatibility` with a branch
|
|
@@ -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
|
|
@@ -30,6 +30,10 @@ import sys
|
|
|
30
30
|
# --- limits, each with the authority that sets it --------------------------
|
|
31
31
|
NAME_MAX = 64 # spec: name is 1-64 characters
|
|
32
32
|
DESC_MAX = 1024 # spec: description is 1-1024 characters
|
|
33
|
+
# House working limit: 5% under the cap. The description is the whole triggering
|
|
34
|
+
# budget AND the field that must grow when a near-miss skill appears ("say what
|
|
35
|
+
# it is NOT for"). A description at 98% of cap cannot absorb that sentence.
|
|
36
|
+
DESC_TARGET = 970
|
|
33
37
|
COMPAT_MAX = 500 # spec: compatibility is 1-500 characters
|
|
34
38
|
BODY_MAX_LINES = 500 # spec + Anthropic: keep the body under 500 lines
|
|
35
39
|
BODY_MAX_TOKENS = 5000 # spec + Anthropic: level-2 budget
|
|
@@ -171,7 +175,10 @@ def _check_name(a, fm, lines, dir_name, rel):
|
|
|
171
175
|
a.gap("NAME_CHARSET", "name %r must be lowercase a-z0-9 with single internal "
|
|
172
176
|
"hyphens — no uppercase, no leading/trailing or doubled hyphen" % name, rel, ln)
|
|
173
177
|
else:
|
|
174
|
-
|
|
178
|
+
# Charset only. Saying "spec-legal" here would contradict NAME_RESERVED
|
|
179
|
+
# and NAME_XML below, and a PASS line quoted out of context is exactly
|
|
180
|
+
# how a wrong verdict acquires real command output as its evidence.
|
|
181
|
+
a.ok("NAME_CHARSET", "name %r uses a legal charset" % name, rel, ln)
|
|
175
182
|
if name != dir_name:
|
|
176
183
|
a.gap("NAME_DIR", "name %r != directory %r — the spec requires them to match, "
|
|
177
184
|
"and the Skills API matches the uploaded top-level directory against it"
|
|
@@ -211,10 +218,24 @@ def _check_description(a, fm, lines, rel, house):
|
|
|
211
218
|
else:
|
|
212
219
|
a.ok("DESC_PERSON", "description is third person", rel, ln)
|
|
213
220
|
if house:
|
|
221
|
+
# Both report either way: "the house rules were checked" has to be
|
|
222
|
+
# provable from the output the canon tells the agent to cite.
|
|
214
223
|
if not desc.lower().startswith("use when"):
|
|
215
224
|
a.gap("DESC_USEWHEN", "description must start with 'Use when …' (house rule)", rel, ln)
|
|
225
|
+
else:
|
|
226
|
+
a.ok("DESC_USEWHEN", "description opens with 'Use when …' (house rule)", rel, ln)
|
|
216
227
|
if not re.search(r"[а-яё]", desc, re.I):
|
|
217
228
|
a.gap("DESC_RU", "description carries no Russian trigger phrases (house rule)", rel, ln)
|
|
229
|
+
else:
|
|
230
|
+
a.ok("DESC_RU", "description carries Russian trigger phrases (house rule)", rel, ln)
|
|
231
|
+
if DESC_TARGET < len(desc) <= DESC_MAX:
|
|
232
|
+
a.gap("DESC_HEADROOM", "description is %d chars — inside the %d cap but past the "
|
|
233
|
+
"%d working limit (house rule): leave room for the 'what this is NOT for' "
|
|
234
|
+
"clause a near-miss neighbour will require"
|
|
235
|
+
% (len(desc), DESC_MAX, DESC_TARGET), rel, ln)
|
|
236
|
+
elif len(desc) <= DESC_TARGET:
|
|
237
|
+
a.ok("DESC_HEADROOM", "description is %d/%d chars, inside the working limit"
|
|
238
|
+
% (len(desc), DESC_TARGET), rel, ln)
|
|
218
239
|
|
|
219
240
|
|
|
220
241
|
def _check_optional_fields(a, fm, lines, rel):
|
|
@@ -253,13 +274,18 @@ def _check_keys(a, fm, lines, rel):
|
|
|
253
274
|
def _check_body_budget(a, body, rel):
|
|
254
275
|
n_lines = body.count("\n") + 1
|
|
255
276
|
est = int(len(body) / CHARS_PER_TOKEN)
|
|
277
|
+
# Both, not either: a body over the line budget still has to report its
|
|
278
|
+
# token count, or the second fix arrives only after the first one ships.
|
|
279
|
+
over = False
|
|
256
280
|
if n_lines >= BODY_MAX_LINES:
|
|
257
281
|
a.gap("BODY_LINES", "body is %d lines, the budget is < %d — move detail into "
|
|
258
282
|
"references/" % (n_lines, BODY_MAX_LINES), rel)
|
|
259
|
-
|
|
283
|
+
over = True
|
|
284
|
+
if est >= BODY_MAX_TOKENS:
|
|
260
285
|
a.gap("BODY_TOKENS", "body is ~%d tokens (%d chars / %s), the budget is < %d"
|
|
261
286
|
% (est, len(body), CHARS_PER_TOKEN, BODY_MAX_TOKENS), rel)
|
|
262
|
-
|
|
287
|
+
over = True
|
|
288
|
+
if not over:
|
|
263
289
|
a.ok("BODY_BUDGET", "body is %d lines / ~%d tokens (budget %d / %d)"
|
|
264
290
|
% (n_lines, est, BODY_MAX_LINES, BODY_MAX_TOKENS), rel)
|
|
265
291
|
|
|
@@ -295,21 +321,27 @@ def _check_bundle(a, skill_dir, skill_text, dir_name):
|
|
|
295
321
|
|
|
296
322
|
|
|
297
323
|
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
|
-
|
|
324
|
+
"""A relative link that escapes the skill directory arrives broken everywhere.
|
|
325
|
+
|
|
326
|
+
Reported per line: the canon defines evidence as a `file:line`, so a finding
|
|
327
|
+
without one forces the agent to either drop the evidence or invent it.
|
|
328
|
+
"""
|
|
329
|
+
bad = False
|
|
330
|
+
for i, line in enumerate(skill_text.split("\n"), start=1):
|
|
331
|
+
for target in LINK_RE.findall(line):
|
|
332
|
+
if target.startswith(("http://", "https://", "mailto:", "#")):
|
|
333
|
+
continue
|
|
334
|
+
if target.startswith("../") or "/../" in target:
|
|
335
|
+
a.gap("LINK_ESCAPE", "link %r escapes the skill directory — packagers "
|
|
336
|
+
"ship that directory alone, so it arrives broken on every agent"
|
|
337
|
+
% target, rel, i)
|
|
338
|
+
bad = True
|
|
339
|
+
continue
|
|
340
|
+
path = os.path.normpath(os.path.join(skill_dir, target.split("#")[0]))
|
|
341
|
+
if not os.path.exists(path):
|
|
342
|
+
a.gap("LINK_BROKEN", "link %r does not resolve" % target, rel, i)
|
|
343
|
+
bad = True
|
|
344
|
+
if not bad:
|
|
313
345
|
a.ok("LINK_INTEGRITY", "every relative link resolves and stays inside the skill", rel)
|
|
314
346
|
|
|
315
347
|
|
|
@@ -335,8 +367,11 @@ def _check_prose(a, body, rel):
|
|
|
335
367
|
a.gap("TIME_BRANCH", "time-branching instruction (%r) — it is wrong the day "
|
|
336
368
|
"it ships; put superseded material under '## Old patterns'"
|
|
337
369
|
% raw.strip()[:60], rel, i)
|
|
338
|
-
#
|
|
339
|
-
|
|
370
|
+
# A pointer at a directory rather than a file with a load condition. Quoted
|
|
371
|
+
# spans are blanked here too: a canon that forbids "see references/" has to
|
|
372
|
+
# be able to quote the phrase it forbids.
|
|
373
|
+
for i, raw in enumerate(body.split("\n"), start=1):
|
|
374
|
+
line = _strip_quoted(raw)
|
|
340
375
|
if re.search(r"\b(?:see|read)\s+`?(?:references|scripts|assets)/`?(?![\w.-])", line, re.I):
|
|
341
376
|
a.gap("REF_NO_TRIGGER", "points at a directory instead of a file with a "
|
|
342
377
|
"stated load condition — 'read X when Y' beats 'see references/'", rel, i)
|