@ssheleg/make-skill 0.29.0 → 0.29.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
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## v0.29.1 — indented plain descriptions are measured correctly
|
|
2
|
+
|
|
3
|
+
The auditor now accepts a plain YAML description whose text begins on the
|
|
4
|
+
next indented line. It folds continuation lines, preserves paragraph breaks,
|
|
5
|
+
and respects comment boundaries instead of reporting a valid description as
|
|
6
|
+
missing. Bare typed values remain subject to the field's string requirement.
|
|
7
|
+
|
|
8
|
+
Unsupported forms in that path produce `FM_SUBSET_UNSUPPORTED`; field checks
|
|
9
|
+
remain unmeasured instead of making a missing-value claim. The parser stays
|
|
10
|
+
dependency-free, with an installed full YAML parser used only as an optional
|
|
11
|
+
oracle. The regression covers valid strings, actual missing/invalid values,
|
|
12
|
+
length limits, metadata maps, comments and the no-PyYAML path.
|
|
13
|
+
|
|
1
14
|
## v0.29.0 — the standard now reads a hook key the host ignores
|
|
2
15
|
|
|
3
16
|
Claude Code 2.1.270 began printing `hooks.json: unknown key "if" in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/make-skill",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"skill",
|
|
@@ -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.29.
|
|
6
|
+
"version": "0.29.1",
|
|
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.29.
|
|
8
|
+
version: "0.29.1"
|
|
9
9
|
homepage: https://github.com/ssheleg/make-skill
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -303,7 +303,8 @@ def parse_frontmatter(text):
|
|
|
303
303
|
"""A STRICT stdlib precheck of an explicitly bounded YAML subset — NOT full
|
|
304
304
|
YAML, and it does not pretend to be (FIX-MS-02.01).
|
|
305
305
|
|
|
306
|
-
The supported subset: top-level scalars (quoted or plain
|
|
306
|
+
The supported subset: top-level scalars (quoted or plain, including a plain
|
|
307
|
+
value starting on the next indented line), block scalars
|
|
307
308
|
(`>`/`|` and their chomping variants), inline flow sequences (`[a, b]`),
|
|
308
309
|
and ONE nested map. Everything else is out of subset and must be caught,
|
|
309
310
|
never waved through — a precheck that silently accepts what the real
|
|
@@ -325,12 +326,34 @@ def parse_frontmatter(text):
|
|
|
325
326
|
for a skill the Skills API rejects on upload (2026-08-16, B-63).
|
|
326
327
|
"""
|
|
327
328
|
parse_frontmatter.last_duplicates = []
|
|
329
|
+
parse_frontmatter.last_unsupported = {}
|
|
328
330
|
data, lines, key, mode = {}, {}, None, None
|
|
329
331
|
scalars = set()
|
|
330
332
|
duplicates = [] # (scope, key) pairs seen more than once
|
|
331
333
|
nested_seen = set()
|
|
334
|
+
blank_lines = 0
|
|
335
|
+
plain_closed = False
|
|
336
|
+
|
|
337
|
+
def plain_part(value):
|
|
338
|
+
# Only plain scalars: quotes and flow values use their existing path.
|
|
339
|
+
# A hash in a URL is data; whitespace followed by # starts a comment.
|
|
340
|
+
m = re.search(r"(?:^|\s)#", value)
|
|
341
|
+
return (value[:m.start()].rstrip(), True) if m else (value, False)
|
|
342
|
+
|
|
343
|
+
def unsupported(reason):
|
|
344
|
+
parse_frontmatter.last_unsupported[key] = reason
|
|
345
|
+
data[key] = None
|
|
346
|
+
scalars.discard(key)
|
|
347
|
+
|
|
332
348
|
for i, raw in enumerate(text.split("\n"), start=2): # +2: the opening '---'
|
|
333
349
|
if not raw.strip():
|
|
350
|
+
if mode == "plain":
|
|
351
|
+
blank_lines += 1
|
|
352
|
+
continue
|
|
353
|
+
if raw.lstrip().startswith("#") and (
|
|
354
|
+
raw[0] not in " \t" or mode in (None, "pending", "plain", "map")):
|
|
355
|
+
if mode == "plain":
|
|
356
|
+
plain_closed = True
|
|
334
357
|
continue
|
|
335
358
|
if raw[0] not in " \t":
|
|
336
359
|
m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", raw)
|
|
@@ -342,10 +365,14 @@ def parse_frontmatter(text):
|
|
|
342
365
|
duplicates.append(("top", key))
|
|
343
366
|
nested_seen = set()
|
|
344
367
|
lines[key] = i
|
|
368
|
+
blank_lines, plain_closed = 0, False
|
|
369
|
+
# Delay the map/scalar decision: `description:` can introduce either.
|
|
370
|
+
if val.startswith("#"):
|
|
371
|
+
val = ""
|
|
345
372
|
if val in (">", "|", ">-", "|-", ">+", "|+"):
|
|
346
373
|
data[key], mode = "", "block"
|
|
347
374
|
elif val == "":
|
|
348
|
-
data[key], mode =
|
|
375
|
+
data[key], mode = None, "pending"
|
|
349
376
|
else:
|
|
350
377
|
# Kept RAW here and finished at the end: a quoted scalar that
|
|
351
378
|
# spans lines carries its closing quote on the last one, so
|
|
@@ -353,10 +380,38 @@ def parse_frontmatter(text):
|
|
|
353
380
|
# buried in the middle of the folded value.
|
|
354
381
|
data[key], mode = val, "scalar"
|
|
355
382
|
scalars.add(key)
|
|
383
|
+
if val[0] not in "\"'[{":
|
|
384
|
+
data[key], plain_closed = plain_part(val)
|
|
385
|
+
mode = "plain"
|
|
386
|
+
elif mode == "pending":
|
|
387
|
+
value = raw.strip()
|
|
388
|
+
if re.match(r"^[A-Za-z0-9_-]+:(?:\s|$)", value):
|
|
389
|
+
data[key], mode = {}, "map"
|
|
390
|
+
m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", value)
|
|
391
|
+
nested_seen.add(m.group(1))
|
|
392
|
+
data[key][m.group(1)] = _typed_scalar(m.group(2).strip())
|
|
393
|
+
elif value[0] in "&*!{[|>" or re.match(r"^[-?:](?:\s|$)", value):
|
|
394
|
+
unsupported("indented value uses a YAML form outside the plain-scalar subset")
|
|
395
|
+
mode = "unsupported"
|
|
396
|
+
else:
|
|
397
|
+
data[key], mode = value, "scalar"
|
|
398
|
+
scalars.add(key)
|
|
399
|
+
if value[0] not in "\"'":
|
|
400
|
+
data[key], plain_closed = plain_part(value)
|
|
401
|
+
mode = "plain"
|
|
356
402
|
elif mode == "block":
|
|
357
403
|
data[key] = (data[key] + " " + raw.strip()).strip()
|
|
358
404
|
elif mode == "scalar":
|
|
359
405
|
data[key] = (data[key] + " " + raw.strip()).strip()
|
|
406
|
+
elif mode == "plain":
|
|
407
|
+
value, closed = plain_part(raw.strip())
|
|
408
|
+
if plain_closed or re.search(r":(?:\s|$)", value):
|
|
409
|
+
unsupported("plain scalar continuation follows a comment or contains a mapping separator")
|
|
410
|
+
mode = "unsupported"
|
|
411
|
+
else:
|
|
412
|
+
separator = "\n" * blank_lines if blank_lines else " "
|
|
413
|
+
data[key] += separator + value
|
|
414
|
+
blank_lines, plain_closed = 0, closed
|
|
360
415
|
elif mode == "map":
|
|
361
416
|
m = re.match(r"^\s+([A-Za-z0-9_-]+):\s*(.*)$", raw)
|
|
362
417
|
if m:
|
|
@@ -405,7 +460,7 @@ def _finish_scalar(v):
|
|
|
405
460
|
if len(v) >= 2 and v[0] == "[" and v[-1] == "]":
|
|
406
461
|
inner = v[1:-1].strip()
|
|
407
462
|
return [] if not inner else [_unquote(p.strip()) for p in inner.split(",")]
|
|
408
|
-
return
|
|
463
|
+
return _typed_scalar(v)
|
|
409
464
|
|
|
410
465
|
|
|
411
466
|
def _unquote(v):
|
|
@@ -436,6 +491,7 @@ def audit(skill_dir, house=False):
|
|
|
436
491
|
"the file, delimited by ---", rel, 1)
|
|
437
492
|
return a
|
|
438
493
|
fm, fm_lines = parse_frontmatter(m.group(1))
|
|
494
|
+
unsupported = dict(getattr(parse_frontmatter, "last_unsupported", {}))
|
|
439
495
|
body = text[m.end():]
|
|
440
496
|
|
|
441
497
|
dups = getattr(parse_frontmatter, "last_duplicates", [])
|
|
@@ -461,9 +517,16 @@ def audit(skill_dir, house=False):
|
|
|
461
517
|
a.ok("FM_YAML_CONFORMANCE", "the subset parse matches the installed "
|
|
462
518
|
"YAML parser", rel)
|
|
463
519
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
520
|
+
for key, reason in unsupported.items():
|
|
521
|
+
a.gap("FM_SUBSET_UNSUPPORTED", "%s: %s; field checks are unmeasured, "
|
|
522
|
+
"not evidence of a missing or invalid value" % (key, reason),
|
|
523
|
+
rel, fm_lines.get(key))
|
|
524
|
+
if "name" not in unsupported:
|
|
525
|
+
_check_name(a, fm, fm_lines, name_on_disk, rel)
|
|
526
|
+
if "description" not in unsupported:
|
|
527
|
+
_check_description(a, fm, fm_lines, rel, house)
|
|
528
|
+
measured = {k: v for k, v in fm.items() if k not in unsupported}
|
|
529
|
+
_check_optional_fields(a, measured, fm_lines, rel)
|
|
467
530
|
_check_keys(a, fm, fm_lines, rel)
|
|
468
531
|
_check_body_budget(a, body, rel, house)
|
|
469
532
|
_check_bundle(a, skill_dir, text, name_on_disk)
|