@ssheleg/make-skill 0.10.0 → 0.11.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.
@@ -19,14 +19,14 @@ schema is dated by design.
19
19
 
20
20
  ## Contents
21
21
 
22
- - [Mounting into an existing web app](#mounting-into-an-existing-web-app)
23
- - [Auth middleware and a health endpoint](#auth-middleware-and-a-health-endpoint)
24
- - [Client configuration](#client-configuration)
25
- - [Debugging a client that will not connect](#debugging-a-client-that-will-not-connect)
26
- - [Publishing to the MCP Registry](#publishing-to-the-mcp-registry)
27
- - [Registry name formats and who may claim them](#registry-name-formats-and-who-may-claim-them)
28
- - [Automating publication](#automating-publication)
29
- - [Versioning](#versioning)
22
+ - Mounting into an existing web app
23
+ - Auth middleware and a health endpoint
24
+ - Client configuration
25
+ - Debugging a client that will not connect
26
+ - Publishing to the MCP Registry
27
+ - Registry name formats and who may claim them
28
+ - Automating publication
29
+ - Versioning
30
30
 
31
31
  ---
32
32
 
@@ -14,18 +14,33 @@ standard — including the no-argument `/make-skill` path, where a `SKILL.md`,
14
14
 
15
15
  ## Run the script first
16
16
 
17
- The mechanical half is deterministic — do not reason your way through it:
17
+ The mechanical half is deterministic — do not reason your way through it. In
18
+ Claude Code the plugin's `bin/` is on the Bash tool's PATH, so the auditor is
19
+ reachable by name:
18
20
 
19
21
  ```bash
20
- python3 "${CLAUDE_PLUGIN_ROOT}/skills/make-skill/scripts/audit_skill.py" <skill-dir> --house
22
+ make-skill-audit <skill-dir> --house
21
23
  ```
22
24
 
23
- `${CLAUDE_PLUGIN_ROOT}` is Claude Code's. On any other host the script sits at
24
- `scripts/audit_skill.py` inside the make-skill directory you are reading this
25
- from (`~/.agents/skills/make-skill/`, `~/.claude/skills/make-skill/`). No
26
- `python3` at all? Check items 1 and 9 below by hand against
27
- `references/agent-skills-spec.md` — the script implements that checklist and
28
- nothing more. Either way, the judgement items are yours.
25
+ On any other host, run the script it wraps — `scripts/audit_skill.py` inside the
26
+ make-skill directory you are reading this from (`~/.agents/skills/make-skill/`,
27
+ `~/.claude/skills/make-skill/`):
28
+
29
+ ```bash
30
+ python3 <make-skill dir>/scripts/audit_skill.py <skill-dir> --house
31
+ ```
32
+
33
+ **Do not build the command out of `${CLAUDE_PLUGIN_ROOT}`.** That variable is
34
+ substituted into skill, command and agent *text*, but it is **not exported to
35
+ the Bash tool's environment** — a command written around it expands to
36
+ `/skills/...` and dies with "No such file or directory" (measured, Claude Code
37
+ 2.1.220). That failure is how a mechanical check turns into a reasoned-about
38
+ PASS, which is the one outcome this file exists to prevent.
39
+
40
+ No `python3` at all? Record items 1 and 9 as **NOT-RUN** with that reason and
41
+ check them by hand against `references/agent-skills-spec.md` — the script
42
+ implements that checklist and nothing more. Either way, the judgement items are
43
+ yours.
29
44
 
30
45
  ## How to report
31
46
 
@@ -35,11 +50,21 @@ Audit first, fix second, in the same session. One verdict per item:
35
50
  |---|---|
36
51
  | **PASS** | the check was actually run — a command's output or a `file:line` |
37
52
  | **GAP** | what is wrong, the evidence, and the fix that closes it |
53
+ | **NOT-RUN** | the tool the check needs is absent — name the tool and the reason |
38
54
 
39
55
  "Looks fine" is not a verdict. Neither is a PASS on a check that was reasoned
40
56
  about rather than executed: `python3 test/validate.py`, `claude plugin validate
41
57
  … --strict`, `npx skills add <repo> --list` all produce output, and the output is
42
- the evidence. Report the gap table before changing anything, then fix.
58
+ the evidence.
59
+
60
+ **NOT-RUN exists so that "I could not run this" has somewhere to go.** A
61
+ checklist with only PASS and GAP forces an unrunnable check into one of them,
62
+ and the cheaper lie is PASS. Item 1's `skills-ref validate` is the standing
63
+ example: it installs from source only, so on most machines it is NOT-RUN, and
64
+ saying so is the honest result. A NOT-RUN you did not try to resolve is still a
65
+ gap in the audit — say what would make it runnable.
66
+
67
+ Report the table before changing anything, then fix.
43
68
 
44
69
  ## The 14-item audit checklist
45
70
 
@@ -84,15 +109,17 @@ the evidence. Report the gap table before changing anything, then fix.
84
109
  skills add <repo> --list` lists ONLY real skills; `npx <name>` works from a
85
110
  non-repo cwd if it is published; `.mdc` rules valid and free of relative
86
111
  links.
87
- 11. **Repo meta**: homepage, description and topics set on the forge; LICENSE
112
+ 11. **Repo meta**: homepage, description and topics set on the forge (any
113
+ consistent homepage convention — do not "fix" a deliberate one); LICENSE
88
114
  present and declared in the front matter and the marketplace entry;
89
115
  CHANGELOG current; public repos also carry CONTRIBUTING.md and SECURITY.md.
90
116
  12. **Gotcha compliance**: the list in `SKILL.md`, plus the installer traps in
91
117
  `references/distribution.md` when the repo ships a CLI or a validator.
92
118
  13. **Protocol dependencies** if it touches MCP or A2A (`references/mcp.md`,
93
- `references/a2a.md`): dependency and protocol version in `compatibility`,
94
- discovery instead of hardcoded tool names, the untrusted-output rule stated,
95
- interactive auth handled as a human step rather than a retry loop.
119
+ `references/mcp-ship.md` once a server is being shipped, `references/a2a.md`):
120
+ dependency and protocol version in `compatibility`, discovery instead of
121
+ hardcoded tool names, the untrusted-output rule stated, interactive auth
122
+ handled as a human step rather than a retry loop.
96
123
  14. **Host capabilities and their fallbacks**
97
124
  (`references/host-capabilities.md`) if it ships a hook, subagent, command or
98
125
  MCP server: the degradation contract written in the body for all three axes
@@ -171,7 +171,10 @@ def _check_name(a, fm, lines, dir_name, rel):
171
171
  a.gap("NAME_CHARSET", "name %r must be lowercase a-z0-9 with single internal "
172
172
  "hyphens — no uppercase, no leading/trailing or doubled hyphen" % name, rel, ln)
173
173
  else:
174
- a.ok("NAME_CHARSET", "name %r is spec-legal" % name, rel, ln)
174
+ # Charset only. Saying "spec-legal" here would contradict NAME_RESERVED
175
+ # and NAME_XML below, and a PASS line quoted out of context is exactly
176
+ # how a wrong verdict acquires real command output as its evidence.
177
+ a.ok("NAME_CHARSET", "name %r uses a legal charset" % name, rel, ln)
175
178
  if name != dir_name:
176
179
  a.gap("NAME_DIR", "name %r != directory %r — the spec requires them to match, "
177
180
  "and the Skills API matches the uploaded top-level directory against it"
@@ -211,10 +214,16 @@ def _check_description(a, fm, lines, rel, house):
211
214
  else:
212
215
  a.ok("DESC_PERSON", "description is third person", rel, ln)
213
216
  if house:
217
+ # Both report either way: "the house rules were checked" has to be
218
+ # provable from the output the canon tells the agent to cite.
214
219
  if not desc.lower().startswith("use when"):
215
220
  a.gap("DESC_USEWHEN", "description must start with 'Use when …' (house rule)", rel, ln)
221
+ else:
222
+ a.ok("DESC_USEWHEN", "description opens with 'Use when …' (house rule)", rel, ln)
216
223
  if not re.search(r"[а-яё]", desc, re.I):
217
224
  a.gap("DESC_RU", "description carries no Russian trigger phrases (house rule)", rel, ln)
225
+ else:
226
+ a.ok("DESC_RU", "description carries Russian trigger phrases (house rule)", rel, ln)
218
227
 
219
228
 
220
229
  def _check_optional_fields(a, fm, lines, rel):
@@ -253,13 +262,18 @@ def _check_keys(a, fm, lines, rel):
253
262
  def _check_body_budget(a, body, rel):
254
263
  n_lines = body.count("\n") + 1
255
264
  est = int(len(body) / CHARS_PER_TOKEN)
265
+ # Both, not either: a body over the line budget still has to report its
266
+ # token count, or the second fix arrives only after the first one ships.
267
+ over = False
256
268
  if n_lines >= BODY_MAX_LINES:
257
269
  a.gap("BODY_LINES", "body is %d lines, the budget is < %d — move detail into "
258
270
  "references/" % (n_lines, BODY_MAX_LINES), rel)
259
- elif est >= BODY_MAX_TOKENS:
271
+ over = True
272
+ if est >= BODY_MAX_TOKENS:
260
273
  a.gap("BODY_TOKENS", "body is ~%d tokens (%d chars / %s), the budget is < %d"
261
274
  % (est, len(body), CHARS_PER_TOKEN, BODY_MAX_TOKENS), rel)
262
- else:
275
+ over = True
276
+ if not over:
263
277
  a.ok("BODY_BUDGET", "body is %d lines / ~%d tokens (budget %d / %d)"
264
278
  % (n_lines, est, BODY_MAX_LINES, BODY_MAX_TOKENS), rel)
265
279
 
@@ -295,21 +309,27 @@ def _check_bundle(a, skill_dir, skill_text, dir_name):
295
309
 
296
310
 
297
311
  def _check_links(a, skill_dir, skill_text, rel):
298
- """A relative link that escapes the skill directory arrives broken everywhere."""
299
- escaped = broken = False
300
- for target in LINK_RE.findall(skill_text):
301
- if target.startswith(("http://", "https://", "mailto:", "#")):
302
- continue
303
- if target.startswith("../") or "/../" in target:
304
- a.gap("LINK_ESCAPE", "link %r escapes the skill directory — packagers ship "
305
- "that directory alone, so it arrives broken on every agent" % target, rel)
306
- escaped = True
307
- continue
308
- path = os.path.normpath(os.path.join(skill_dir, target.split("#")[0]))
309
- if not os.path.exists(path):
310
- a.gap("LINK_BROKEN", "link %r does not resolve" % target, rel)
311
- broken = True
312
- if not escaped and not broken:
312
+ """A relative link that escapes the skill directory arrives broken everywhere.
313
+
314
+ Reported per line: the canon defines evidence as a `file:line`, so a finding
315
+ without one forces the agent to either drop the evidence or invent it.
316
+ """
317
+ bad = False
318
+ for i, line in enumerate(skill_text.split("\n"), start=1):
319
+ for target in LINK_RE.findall(line):
320
+ if target.startswith(("http://", "https://", "mailto:", "#")):
321
+ continue
322
+ if target.startswith("../") or "/../" in target:
323
+ a.gap("LINK_ESCAPE", "link %r escapes the skill directory — packagers "
324
+ "ship that directory alone, so it arrives broken on every agent"
325
+ % target, rel, i)
326
+ bad = True
327
+ continue
328
+ path = os.path.normpath(os.path.join(skill_dir, target.split("#")[0]))
329
+ if not os.path.exists(path):
330
+ a.gap("LINK_BROKEN", "link %r does not resolve" % target, rel, i)
331
+ bad = True
332
+ if not bad:
313
333
  a.ok("LINK_INTEGRITY", "every relative link resolves and stays inside the skill", rel)
314
334
 
315
335
 
@@ -335,8 +355,11 @@ def _check_prose(a, body, rel):
335
355
  a.gap("TIME_BRANCH", "time-branching instruction (%r) — it is wrong the day "
336
356
  "it ships; put superseded material under '## Old patterns'"
337
357
  % raw.strip()[:60], rel, i)
338
- # a pointer at a directory rather than a file with a load condition
339
- for i, line in enumerate(body.split("\n"), start=1):
358
+ # A pointer at a directory rather than a file with a load condition. Quoted
359
+ # spans are blanked here too: a canon that forbids "see references/" has to
360
+ # be able to quote the phrase it forbids.
361
+ for i, raw in enumerate(body.split("\n"), start=1):
362
+ line = _strip_quoted(raw)
340
363
  if re.search(r"\b(?:see|read)\s+`?(?:references|scripts|assets)/`?(?![\w.-])", line, re.I):
341
364
  a.gap("REF_NO_TRIGGER", "points at a directory instead of a file with a "
342
365
  "stated load condition — 'read X when Y' beats 'see references/'", rel, i)