@ssheleg/make-skill 0.19.1 → 0.21.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 +80 -0
- package/package.json +3 -3
- package/plugins/make-skill/.claude-plugin/plugin.json +1 -1
- package/plugins/make-skill/skills/make-skill/SKILL.md +1 -1
- package/plugins/make-skill/skills/make-skill/references/authoring.md +38 -0
- package/plugins/make-skill/skills/make-skill/scripts/audit_skill.py +61 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.21.0 — a gate's self-test runs in both directions
|
|
4
|
+
|
|
5
|
+
*"A validator that can't fail is decoration"* has been the rule here since the standard was
|
|
6
|
+
written. It is **half** the rule, and the missing half is the expensive one.
|
|
7
|
+
|
|
8
|
+
The planted defect proves a gate CAN fire. Nothing in it proves the gate fires only where it
|
|
9
|
+
should — and a gate that flags correct code is switched off within a day, taking the real check
|
|
10
|
+
with it. **A false positive does not arrive as a bug report. It arrives as an argument about
|
|
11
|
+
your gate**, from someone whose code was fine.
|
|
12
|
+
|
|
13
|
+
`references/authoring.md` gains *A gate's self-test runs in both directions*: the two kinds of
|
|
14
|
+
case, what each one's absence costs, and the point that counter-shapes **decide** a gate's
|
|
15
|
+
design rather than merely verifying it — a pattern that finds five hits and is wrong about all
|
|
16
|
+
five is a parser you have not written yet.
|
|
17
|
+
|
|
18
|
+
Two recurring shapes are named because both have shipped:
|
|
19
|
+
|
|
20
|
+
- **The input that cannot be resolved** must fail loudly, never skip — *a check that cannot run
|
|
21
|
+
is not a check that passed*. A slug resolver anchored on `github.com` returned nothing for
|
|
22
|
+
npm's `github:owner/repo` shorthand, so its check printed `skip` and reported success for work
|
|
23
|
+
it never did.
|
|
24
|
+
- **The word inside another word** — `lease` in *please*, `аудит` in *аудитория*, `seo` in
|
|
25
|
+
*Seoul*. Found by running the gate over a real corpus, never by reading it.
|
|
26
|
+
|
|
27
|
+
And: **count the cases, do not restate the count.** A summary saying *"5 cases"* while thirteen
|
|
28
|
+
run is the defect the gate exists to prevent, in the gate's own output.
|
|
29
|
+
|
|
30
|
+
**The doctrine went to the reference rather than the body, and the body's own gate decided
|
|
31
|
+
that.** `SKILL.md` measured 4738 of a 4750 working limit — twelve tokens of headroom — so every
|
|
32
|
+
draft that stated the rule inline failed the budget check. `authoring.md` is linked from
|
|
33
|
+
`SKILL.md` twice, so the rule is reachable without spending a body it does not have. Closes #5.
|
|
34
|
+
|
|
35
|
+
## v0.20.0 — the checker said 180 where the file said 1392
|
|
36
|
+
|
|
37
|
+
**Both checkers dropped the continuation lines of a plain multi-line YAML
|
|
38
|
+
description**, so a description whose real length was **1392 characters was
|
|
39
|
+
measured at 180** and passed the 1024 spec cap, the 970 working limit and every
|
|
40
|
+
`/skill-audit` this family issues. The standard-keeper handed a clean bill to a
|
|
41
|
+
skill the Skills API rejects on upload, and `make-skill`'s own gate could not
|
|
42
|
+
catch it either. A plain scalar may continue on indented lines and YAML folds
|
|
43
|
+
them into one value with a space; `parse_frontmatter` discarded them, in both
|
|
44
|
+
`scripts/audit_skill.py` and `test/validate.py`, byte for byte the same defect.
|
|
45
|
+
|
|
46
|
+
**`allowed-tools: [Read, Write]` was read as the string `"[Read, Write]"`.**
|
|
47
|
+
`TOOLS_TYPE` asks whether the value is a `str` and got yes, so the check written
|
|
48
|
+
for exactly that form never fired — on the most common way authors write a tool
|
|
49
|
+
list, and the one portability defect that costs a skill its tool grant on every
|
|
50
|
+
host but Claude Code. A flow sequence now parses to a list, and the check fires.
|
|
51
|
+
|
|
52
|
+
**The scalar is kept raw and finished at the end**, because a quoted scalar
|
|
53
|
+
spanning lines carries its closing quote on the last one: unquoting the first
|
|
54
|
+
line strips nothing and buries the quote in the middle of the folded value.
|
|
55
|
+
|
|
56
|
+
**`--house` now applies the body's half of the 5% rule.** The canon states the
|
|
57
|
+
headroom as one sentence for both fields, and this script applied it to the
|
|
58
|
+
description only — so a body at 4999 tokens got `0 GAP` here and was refused by
|
|
59
|
+
this repo's own validator. Two verdicts on one rule, and the permissive one was
|
|
60
|
+
the one users got. `BODY_HEADROOM` is the new gap id; the `--house` help text
|
|
61
|
+
and the module docstring said neither this nor `DESC_HEADROOM`, and now say both.
|
|
62
|
+
|
|
63
|
+
**The guard against the two checkers drifting compared one of twelve rules.**
|
|
64
|
+
It now compares all nine shared numeric limits by name plus both front-matter key
|
|
65
|
+
sets, and a constant that exists on one side only is a failure rather than a
|
|
66
|
+
silently skipped row — which is exactly how the missing 4750 shipped for four
|
|
67
|
+
releases underneath it. Three limits that were literals at their use sites
|
|
68
|
+
(`NAME_MAX`, `DESC_MAX`, `COMPAT_MAX`) are named, because a number with no name
|
|
69
|
+
has no single home and cannot be compared against a second implementation.
|
|
70
|
+
|
|
71
|
+
**`test/checker_parity_test.py`, 14 cases, joins `npm test`** — locally, not only
|
|
72
|
+
in CI, since the checker it guards runs locally. Watched failing against the
|
|
73
|
+
pre-fix code: **9 of 14 red**. It plants each defect and requires the checker to
|
|
74
|
+
say so, rather than asserting that a correct file passes: a false negative in a
|
|
75
|
+
checker is worse than no checker, because it is a green that gets quoted.
|
|
76
|
+
|
|
77
|
+
**No shipped skill changes verdict.** All 24 in the family re-audited before and
|
|
78
|
+
after: identical, and the one apparent difference was a `__pycache__` this run
|
|
79
|
+
had just created.
|
|
80
|
+
|
|
81
|
+
Found by the nine-repository audit of 2026-08-16 (umbrella `B-63`).
|
|
82
|
+
|
|
3
83
|
## v0.19.1 — 2026-08-16
|
|
4
84
|
|
|
5
85
|
**This gate can now see an invariant it breaks one repository away.** The family umbrella
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/make-skill",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Create, retrofit, audit, and ship agent skills & Claude Code plugins the proven ssheleg way
|
|
3
|
+
"version": "0.21.0",
|
|
4
|
+
"description": "Create, retrofit, audit, and ship agent skills & Claude Code plugins the proven ssheleg way — conformance to the Agent Skills open standard AND Anthropic's platform rules (front-matter limits, disclosure budgets, per-surface runtime limits, the Skills API, evals) plus the Claude Code plugin reference (manifest schemas, component layout, claude plugin validate --strict), marketplace repo layout, version sync, validator + CI, multi-channel distribution (plugin, vercel skills CLI, npx, Cursor), npm gotchas, the review checklist for third-party skills, and MCP / A2A rules for protocol-connected skills. This package is the installer CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"skill",
|
|
7
7
|
"plugin",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"node": ">=16"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
-
"test": "python3 test/validate.py && python3 test/plant_guard_test.py"
|
|
48
|
+
"test": "python3 test/validate.py && python3 test/plant_guard_test.py && python3 test/checker_parity_test.py"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "make-skill",
|
|
4
4
|
"displayName": "Make Skill",
|
|
5
5
|
"description": "Create, retrofit, audit, and ship agent skills & Claude Code plugins the proven ssheleg way: conformance to the Agent Skills open standard, Anthropic's platform rules (surfaces, Skills API, evals) and the Claude Code plugin reference, marketplace repo layout, version sync, validator + CI, multi-channel distribution (plugin, vercel skills CLI, npx, Cursor), npm gotchas, end-to-end first publish, the review checklist for third-party skills, plus MCP / A2A references for protocol-connected skills.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.21.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Authoring works on any agent. The bundled scripts/ need python3. Publishing steps need git, gh, node and npm; the plugin gates need the claude CLI. Not usable on the Claude API surface, which has no network and no runtime package install.
|
|
6
6
|
metadata:
|
|
7
7
|
author: ssheleg
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.21.0"
|
|
9
9
|
homepage: https://github.com/ssheleg/make-skill
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -20,6 +20,7 @@ and [agentskills.io](https://agentskills.io/skill-creation/best-practices).
|
|
|
20
20
|
- Workflows and feedback loops
|
|
21
21
|
- Content guidelines (terminology, time, paths, table of contents)
|
|
22
22
|
- Scripts — the rules that separate a script from a liability
|
|
23
|
+
- A gate's self-test runs in both directions
|
|
23
24
|
- Evaluation and iteration — evals before prose
|
|
24
25
|
- Quality checklist
|
|
25
26
|
|
|
@@ -187,6 +188,43 @@ run flagged a false positive on the very document that teaches the anti-pattern
|
|
|
187
188
|
which is why it now ignores quoted spans. Hooks, subagents and commands that wrap
|
|
188
189
|
it, plus the fallbacks they owe, are in `references/host-capabilities.md`.
|
|
189
190
|
|
|
191
|
+
## A gate's self-test runs in both directions
|
|
192
|
+
|
|
193
|
+
*"A validator that can't fail is decoration"* is half the rule. The planted defect
|
|
194
|
+
proves the gate **can** fire. Nothing in it proves the gate fires only where it
|
|
195
|
+
should — and a gate that flags correct code is switched off within a day, taking
|
|
196
|
+
the real check with it. **A false positive does not arrive as a bug report. It
|
|
197
|
+
arrives as an argument about your gate**, from someone whose code was fine.
|
|
198
|
+
|
|
199
|
+
So a self-test carries two kinds of case:
|
|
200
|
+
|
|
201
|
+
| Direction | The case | What its absence costs |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| **Must catch** | the planted defect, watched failing | a check that cannot fail, green forever |
|
|
204
|
+
| **Must NOT flag** | the legal spelling, the empty input, the near-miss that only resembles the defect | a check that is correct and unusable, removed by the first person it wrongly blocks |
|
|
205
|
+
|
|
206
|
+
**The counter-shapes are also where a gate's design gets decided, not just
|
|
207
|
+
verified.** A pattern that reads *"no key may start with a mounted namespace"*
|
|
208
|
+
finds five hits and all five are correct — they pass absolute keys to a translator
|
|
209
|
+
that prefixes nothing. Written as a pattern the gate is wrong; written as a parser
|
|
210
|
+
that binds each translator variable to its namespace, it is right. The counter-case
|
|
211
|
+
is what tells the two apart, and only running it reveals which one you built.
|
|
212
|
+
|
|
213
|
+
Two shapes recur and both belong in every gate's fixtures:
|
|
214
|
+
|
|
215
|
+
- **The input that cannot be resolved.** A resolver returning nothing must be a
|
|
216
|
+
loud failure, never a silent skip — *a check that cannot run is not a check that
|
|
217
|
+
passed*. A slug resolver anchored on `github.com` returned nothing for npm's
|
|
218
|
+
`github:owner/repo` shorthand, so the check printed `skip` and reported success
|
|
219
|
+
for work it never did.
|
|
220
|
+
- **The word inside another word.** Any matcher over prose needs the near-miss:
|
|
221
|
+
`lease` inside *please*, `аудит` inside *аудитория*, `seo` inside *Seoul*. These
|
|
222
|
+
are found by running the gate over a real corpus, not by reading it.
|
|
223
|
+
|
|
224
|
+
**Count the cases, do not restate the count.** A summary line saying *"5 cases"*
|
|
225
|
+
while thirteen run is the same class of defect the gate exists to prevent, in the
|
|
226
|
+
gate's own output.
|
|
227
|
+
|
|
190
228
|
## Evaluation and iteration — evals before prose
|
|
191
229
|
|
|
192
230
|
**Build the evaluations before writing extensive documentation.** Otherwise the
|
|
@@ -14,8 +14,12 @@ spends its context on the half that needs judgement.
|
|
|
14
14
|
core the Agent Skills open standard (agentskills.io/specification) plus the
|
|
15
15
|
rules Anthropic's platform enforces on upload (reserved words, XML
|
|
16
16
|
tags) and its authoring guidance (third person, tables of contents)
|
|
17
|
-
--house adds the ssheleg canon: description starts "Use when …"
|
|
18
|
-
English AND Russian trigger phrases
|
|
17
|
+
--house adds the ssheleg canon: description starts "Use when …", carries
|
|
18
|
+
English AND Russian trigger phrases, and holds the 5% working
|
|
19
|
+
headroom on BOTH budgets — 970 of 1024 chars, 4750 of 5000 tokens.
|
|
20
|
+
The canon states the headroom as one rule for both fields; applying
|
|
21
|
+
it to one of them is how a body at 4999 tokens got a clean bill here
|
|
22
|
+
and was refused by the repo's own validator.
|
|
19
23
|
|
|
20
24
|
Python 3.9+, standard library only: it has to run wherever the skill landed.
|
|
21
25
|
"""
|
|
@@ -37,6 +41,12 @@ DESC_TARGET = 970
|
|
|
37
41
|
COMPAT_MAX = 500 # spec: compatibility is 1-500 characters
|
|
38
42
|
BODY_MAX_LINES = 500 # spec + Anthropic: keep the body under 500 lines
|
|
39
43
|
BODY_MAX_TOKENS = 5000 # spec + Anthropic: level-2 budget
|
|
44
|
+
# House working limit, the body's half of the same 5% rule DESC_TARGET states.
|
|
45
|
+
# The canon writes both as one sentence — "hold 5% headroom" — and for four
|
|
46
|
+
# releases this script applied only the description half, so a body at 4999
|
|
47
|
+
# tokens got 0 GAP here and was refused by the repo's own validator. Two
|
|
48
|
+
# verdicts on one rule, and the permissive one was the one users got.
|
|
49
|
+
BODY_TARGET_TOKENS = 4750
|
|
40
50
|
# No tokenizer in the stdlib. 3.9 chars/token is measured, not assumed: tokenizing
|
|
41
51
|
# this skill's own bundle gives 3.78-4.47. `claude plugin details` is far more
|
|
42
52
|
# pessimistic (~2.8) and will always show a bigger number than this estimate.
|
|
@@ -95,8 +105,15 @@ def parse_frontmatter(text):
|
|
|
95
105
|
Returns (data, line_of_key). A full YAML parser is not in the stdlib and a
|
|
96
106
|
skill's frontmatter is a flat map by specification, so this is enough — and
|
|
97
107
|
it keeps the script dependency-free, which is the point of shipping it.
|
|
108
|
+
|
|
109
|
+
A plain scalar may continue on indented lines and YAML folds them into one
|
|
110
|
+
value with a space. Dropping those lines is how a description whose real
|
|
111
|
+
length was 1392 characters got measured at 180 and passed both the 1024 cap
|
|
112
|
+
and the 970 working limit — a clean bill from the family's standard-keeper
|
|
113
|
+
for a skill the Skills API rejects on upload (2026-08-16, B-63).
|
|
98
114
|
"""
|
|
99
115
|
data, lines, key, mode = {}, {}, None, None
|
|
116
|
+
scalars = set()
|
|
100
117
|
for i, raw in enumerate(text.split("\n"), start=2): # +2: the opening '---'
|
|
101
118
|
if not raw.strip():
|
|
102
119
|
continue
|
|
@@ -112,16 +129,41 @@ def parse_frontmatter(text):
|
|
|
112
129
|
elif val == "":
|
|
113
130
|
data[key], mode = {}, "map"
|
|
114
131
|
else:
|
|
115
|
-
|
|
132
|
+
# Kept RAW here and finished at the end: a quoted scalar that
|
|
133
|
+
# spans lines carries its closing quote on the last one, so
|
|
134
|
+
# unquoting the first line strips nothing and leaves the quote
|
|
135
|
+
# buried in the middle of the folded value.
|
|
136
|
+
data[key], mode = val, "scalar"
|
|
137
|
+
scalars.add(key)
|
|
116
138
|
elif mode == "block":
|
|
117
139
|
data[key] = (data[key] + " " + raw.strip()).strip()
|
|
140
|
+
elif mode == "scalar":
|
|
141
|
+
data[key] = (data[key] + " " + raw.strip()).strip()
|
|
118
142
|
elif mode == "map":
|
|
119
143
|
m = re.match(r"^\s+([A-Za-z0-9_-]+):\s*(.*)$", raw)
|
|
120
144
|
if m:
|
|
121
145
|
data[key][m.group(1)] = _unquote(m.group(2).strip())
|
|
146
|
+
for k in scalars:
|
|
147
|
+
data[k] = _finish_scalar(data[k])
|
|
122
148
|
return data, lines
|
|
123
149
|
|
|
124
150
|
|
|
151
|
+
def _finish_scalar(v):
|
|
152
|
+
"""A flow sequence is a LIST, not a string that happens to look like one.
|
|
153
|
+
|
|
154
|
+
`allowed-tools: [Read, Write]` read as the string "[Read, Write]" is why
|
|
155
|
+
TOOLS_TYPE never fired: it asks whether the value is a `str`, and the answer
|
|
156
|
+
was yes. The inline sequence is the most common way authors write a tool
|
|
157
|
+
list, and it is the portability defect the rule exists for — the skill works
|
|
158
|
+
in Claude Code and silently loses its tool grant on every other host.
|
|
159
|
+
"""
|
|
160
|
+
v = v.strip()
|
|
161
|
+
if len(v) >= 2 and v[0] == "[" and v[-1] == "]":
|
|
162
|
+
inner = v[1:-1].strip()
|
|
163
|
+
return [] if not inner else [_unquote(p.strip()) for p in inner.split(",")]
|
|
164
|
+
return _unquote(v)
|
|
165
|
+
|
|
166
|
+
|
|
125
167
|
def _unquote(v):
|
|
126
168
|
v = v.strip()
|
|
127
169
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
|
@@ -156,7 +198,7 @@ def audit(skill_dir, house=False):
|
|
|
156
198
|
_check_description(a, fm, fm_lines, rel, house)
|
|
157
199
|
_check_optional_fields(a, fm, fm_lines, rel)
|
|
158
200
|
_check_keys(a, fm, fm_lines, rel)
|
|
159
|
-
_check_body_budget(a, body, rel)
|
|
201
|
+
_check_body_budget(a, body, rel, house)
|
|
160
202
|
_check_bundle(a, skill_dir, text, name_on_disk)
|
|
161
203
|
_check_links(a, skill_dir, text, rel)
|
|
162
204
|
_check_prose(a, body, rel)
|
|
@@ -271,7 +313,7 @@ def _check_keys(a, fm, lines, rel):
|
|
|
271
313
|
a.ok("FM_UNKNOWN_KEY", "no frontmatter key outside spec ∪ host extensions", rel)
|
|
272
314
|
|
|
273
315
|
|
|
274
|
-
def _check_body_budget(a, body, rel):
|
|
316
|
+
def _check_body_budget(a, body, rel, house=False):
|
|
275
317
|
n_lines = body.count("\n") + 1
|
|
276
318
|
est = int(len(body) / CHARS_PER_TOKEN)
|
|
277
319
|
# Both, not either: a body over the line budget still has to report its
|
|
@@ -288,6 +330,17 @@ def _check_body_budget(a, body, rel):
|
|
|
288
330
|
if not over:
|
|
289
331
|
a.ok("BODY_BUDGET", "body is %d lines / ~%d tokens (budget %d / %d)"
|
|
290
332
|
% (n_lines, est, BODY_MAX_LINES, BODY_MAX_TOKENS), rel)
|
|
333
|
+
# The house half, and it is the same rule DESC_HEADROOM applies to the other
|
|
334
|
+
# field: a body at the ceiling cannot absorb the next paragraph, so it gets
|
|
335
|
+
# absorbed into a reference that should have been split instead.
|
|
336
|
+
if house and not over and est >= BODY_TARGET_TOKENS:
|
|
337
|
+
a.gap("BODY_HEADROOM", "body is ~%d tokens — inside the %d budget but past the "
|
|
338
|
+
"%d working limit (house rule): the next section will breach it, and the "
|
|
339
|
+
"answer then is a split, not a trim"
|
|
340
|
+
% (est, BODY_MAX_TOKENS, BODY_TARGET_TOKENS), rel)
|
|
341
|
+
elif house and not over:
|
|
342
|
+
a.ok("BODY_HEADROOM", "body is ~%d/%d tokens, inside the working limit"
|
|
343
|
+
% (est, BODY_TARGET_TOKENS), rel)
|
|
291
344
|
|
|
292
345
|
|
|
293
346
|
def _bundle_closure(skill_dir, skill_text):
|
|
@@ -451,7 +504,9 @@ def main(argv):
|
|
|
451
504
|
description="Audit a skill directory against the Agent Skills standard.")
|
|
452
505
|
p.add_argument("skill_dir", help="the directory containing SKILL.md")
|
|
453
506
|
p.add_argument("--house", action="store_true",
|
|
454
|
-
help="also apply the ssheleg canon
|
|
507
|
+
help="also apply the ssheleg canon: Use-when opener, EN+RU triggers, "
|
|
508
|
+
"and the 5%% working headroom on BOTH the description (970 of "
|
|
509
|
+
"1024) and the body (4750 of 5000)")
|
|
455
510
|
p.add_argument("--json", action="store_true", help="emit results as JSON")
|
|
456
511
|
p.add_argument("--quiet", action="store_true", help="print GAP lines only")
|
|
457
512
|
args = p.parse_args(argv[1:])
|