@ssheleg/make-skill 0.19.0 → 0.20.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 CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.20.0 — the checker said 180 where the file said 1392
4
+
5
+ **Both checkers dropped the continuation lines of a plain multi-line YAML
6
+ description**, so a description whose real length was **1392 characters was
7
+ measured at 180** and passed the 1024 spec cap, the 970 working limit and every
8
+ `/skill-audit` this family issues. The standard-keeper handed a clean bill to a
9
+ skill the Skills API rejects on upload, and `make-skill`'s own gate could not
10
+ catch it either. A plain scalar may continue on indented lines and YAML folds
11
+ them into one value with a space; `parse_frontmatter` discarded them, in both
12
+ `scripts/audit_skill.py` and `test/validate.py`, byte for byte the same defect.
13
+
14
+ **`allowed-tools: [Read, Write]` was read as the string `"[Read, Write]"`.**
15
+ `TOOLS_TYPE` asks whether the value is a `str` and got yes, so the check written
16
+ for exactly that form never fired — on the most common way authors write a tool
17
+ list, and the one portability defect that costs a skill its tool grant on every
18
+ host but Claude Code. A flow sequence now parses to a list, and the check fires.
19
+
20
+ **The scalar is kept raw and finished at the end**, because a quoted scalar
21
+ spanning lines carries its closing quote on the last one: unquoting the first
22
+ line strips nothing and buries the quote in the middle of the folded value.
23
+
24
+ **`--house` now applies the body's half of the 5% rule.** The canon states the
25
+ headroom as one sentence for both fields, and this script applied it to the
26
+ description only — so a body at 4999 tokens got `0 GAP` here and was refused by
27
+ this repo's own validator. Two verdicts on one rule, and the permissive one was
28
+ the one users got. `BODY_HEADROOM` is the new gap id; the `--house` help text
29
+ and the module docstring said neither this nor `DESC_HEADROOM`, and now say both.
30
+
31
+ **The guard against the two checkers drifting compared one of twelve rules.**
32
+ It now compares all nine shared numeric limits by name plus both front-matter key
33
+ sets, and a constant that exists on one side only is a failure rather than a
34
+ silently skipped row — which is exactly how the missing 4750 shipped for four
35
+ releases underneath it. Three limits that were literals at their use sites
36
+ (`NAME_MAX`, `DESC_MAX`, `COMPAT_MAX`) are named, because a number with no name
37
+ has no single home and cannot be compared against a second implementation.
38
+
39
+ **`test/checker_parity_test.py`, 14 cases, joins `npm test`** — locally, not only
40
+ in CI, since the checker it guards runs locally. Watched failing against the
41
+ pre-fix code: **9 of 14 red**. It plants each defect and requires the checker to
42
+ say so, rather than asserting that a correct file passes: a false negative in a
43
+ checker is worse than no checker, because it is a green that gets quoted.
44
+
45
+ **No shipped skill changes verdict.** All 24 in the family re-audited before and
46
+ after: identical, and the one apparent difference was a `__pycache__` this run
47
+ had just created.
48
+
49
+ Found by the nine-repository audit of 2026-08-16 (umbrella `B-63`).
50
+
51
+ ## v0.19.1 — 2026-08-16
52
+
53
+ **This gate can now see an invariant it breaks one repository away.** The family umbrella
54
+ routes work by matching a prompt against a table in `lib/triggers.js`, and every trigger
55
+ there must be a word this skill's own `description` advertises. Nothing here knew that
56
+ table existed. On 2026-08-16 `sheleg-design` 1.37.0 shipped green having dropped a phrase
57
+ that was still a live trigger, the umbrella found out minutes after the tag, and it cost a
58
+ patch release — because the member releases FIRST and the umbrella re-pins after.
59
+
60
+ `test/validate.py` now asks the umbrella's own checker (`test/advertised_check.js`), which
61
+ reads the module the hook itself calls. **No copy of the table lives here**, so there is
62
+ nothing to drift. With no umbrella above this checkout — a standalone clone, and CI — it
63
+ discloses rather than passing, because a check that cannot look must never read as one
64
+ that looked.
65
+
66
+ Watched refusing a real drop before shipping: every one of the seven members carrying
67
+ routed triggers had one of its own advertised phrases removed and every one of them failed
68
+ its own gate.
69
+
3
70
  ## v0.19.0 — 2026-08-16
4
71
 
5
72
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ssheleg/make-skill",
3
- "version": "0.19.0",
4
- "description": "Create, retrofit, audit, and ship agent skills & Claude Code plugins the proven ssheleg way \u2014 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.",
3
+ "version": "0.20.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.19.0",
6
+ "version": "0.20.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.19.0"
8
+ version: "0.20.0"
9
9
  homepage: https://github.com/ssheleg/make-skill
10
10
  ---
11
11
 
@@ -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 …" and carries
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
- data[key], mode = _unquote(val), None
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 (Use-when opener, EN+RU triggers)")
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:])