@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.0",
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.0",
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.0"
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), block scalars
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 = {}, "map"
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 _unquote(v)
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
- _check_name(a, fm, fm_lines, name_on_disk, rel)
465
- _check_description(a, fm, fm_lines, rel, house)
466
- _check_optional_fields(a, fm, fm_lines, rel)
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)