@ssheleg/make-skill 0.12.0 → 0.13.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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.13.0
4
+
5
+ ### Fixed
6
+
7
+ - **`BUNDLE_UNREACHABLE` read reachability as one hop and called 29 correctly-shipped
8
+ contracts dead weight.** The check asked whether `references/foo.md` appears as a
9
+ substring of `SKILL.md`. But a packager ships a skill's directory alone, so a
10
+ reference that links a sibling contract makes that sibling *required* — omit it and
11
+ the first arrives with a dangling link. Reachability is therefore the transitive
12
+ closure, which is exactly what `super-ux`'s own sync script computes, and the check
13
+ disagreed with the mechanism it was auditing.
14
+
15
+ Measured on `super-ux`: 29 → 0, with every other repository in the family byte-identical
16
+ before and after. Probed both ways — a planted orphan is still flagged, and a file
17
+ reachable only through another reference is not. The failing example was
18
+ `ux-flows/references/figma-structure.md`: no direct link from `SKILL.md`, linked by
19
+ `figma-integration.md`, `best-practices.md` and `system-map.md`, all three of which
20
+ `SKILL.md` links directly.
21
+
22
+ - **The false positive was also masking real findings.** An entry flagged unreachable
23
+ `continue`d past the `REF_NO_TOC` check, so a 312-line reference with no `## Contents`
24
+ was invisible behind a gap that should never have fired. Fixing the reachability
25
+ surfaced four of those — the correct behaviour, and the reason a false positive is not
26
+ merely noise.
27
+
28
+ **Minor rather than patch**: for a tool whose output *is* its product, changing what it
29
+ reports changes what everyone downstream sees, and one of those changes is new findings
30
+ appearing on repositories that were green.
31
+
3
32
  ## v0.12.0
4
33
 
5
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/make-skill",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
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",
@@ -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.12.0",
6
+ "version": "0.13.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.12.0"
8
+ version: "0.13.0"
9
9
  homepage: https://github.com/ssheleg/make-skill
10
10
  ---
11
11
 
@@ -290,8 +290,53 @@ def _check_body_budget(a, body, rel):
290
290
  % (n_lines, est, BODY_MAX_LINES, BODY_MAX_TOKENS), rel)
291
291
 
292
292
 
293
+ def _bundle_closure(skill_dir, skill_text):
294
+ """Bundled files reachable from SKILL.md, following links between them.
295
+
296
+ Reachability is transitive, not one hop. A reference that links a sibling contract
297
+ makes that sibling reachable, and shipping the first without the second is what
298
+ leaves a dangling link on the target agent — so a packager that computes what a
299
+ skill needs has to take the closure, and the check has to agree with it. Measured
300
+ against super-ux, where the one-hop reading called 29 transitively-linked contracts
301
+ dead weight.
302
+ """
303
+ available = {}
304
+ for sub in BUNDLE_DIRS:
305
+ d = os.path.join(skill_dir, sub)
306
+ if not os.path.isdir(d):
307
+ continue
308
+ for entry in os.listdir(d):
309
+ if os.path.isfile(os.path.join(d, entry)):
310
+ available["%s/%s" % (sub, entry)] = os.path.join(d, entry)
311
+
312
+ seen = set()
313
+ stack = [key for key in available if key in skill_text]
314
+ while stack:
315
+ key = stack.pop()
316
+ if key in seen:
317
+ continue
318
+ seen.add(key)
319
+ if not key.endswith(".md"):
320
+ continue
321
+ sub = key.split("/", 1)[0]
322
+ try:
323
+ txt = open(available[key], encoding="utf-8").read()
324
+ except (OSError, UnicodeDecodeError):
325
+ continue
326
+ for target in LINK_RE.findall(txt):
327
+ t = target.split("#", 1)[0].strip().lstrip("./")
328
+ if not t or "://" in t:
329
+ continue
330
+ # a bare name inside references/ means a sibling of that same directory
331
+ cand = t if "/" in t else "%s/%s" % (sub, t)
332
+ if cand in available and cand not in seen:
333
+ stack.append(cand)
334
+ return seen
335
+
336
+
293
337
  def _check_bundle(a, skill_dir, skill_text, dir_name):
294
338
  """references/ scripts/ assets/: one level deep, reachable, navigable."""
339
+ reachable = _bundle_closure(skill_dir, skill_text)
295
340
  for sub in BUNDLE_DIRS:
296
341
  d = os.path.join(skill_dir, sub)
297
342
  if not os.path.isdir(d):
@@ -304,10 +349,10 @@ def _check_bundle(a, skill_dir, skill_text, dir_name):
304
349
  "deep, or the agent previews them instead of reading them"
305
350
  % (sub, entry), rel)
306
351
  continue
307
- if "%s/%s" % (sub, entry) not in skill_text:
308
- a.gap("BUNDLE_UNREACHABLE", "%s/%s is never referenced from SKILL.md — "
309
- "an unreachable file is dead weight the agent never opens"
310
- % (sub, entry), rel)
352
+ if "%s/%s" % (sub, entry) not in reachable:
353
+ a.gap("BUNDLE_UNREACHABLE", "%s/%s is not reachable from SKILL.md, "
354
+ "directly or through another bundled file — an unreachable file "
355
+ "is dead weight the agent never opens" % (sub, entry), rel)
311
356
  continue
312
357
  if entry.endswith(".md"):
313
358
  txt = open(full, encoding="utf-8").read()