@ssheleg/make-skill 0.10.0 → 0.11.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.
@@ -40,6 +40,7 @@ but did not write is not a fallback.
40
40
  | Capability | Buys you | Costs | Reach for it when |
41
41
  |---|---|---|---|
42
42
  | `scripts/` | determinism, and output instead of code in context | none until run | the agent re-derives the same logic every run |
43
+ | `bin/` | a bundled script reachable **by name** — Claude Code puts it on the Bash tool's PATH | none until run | a documented command must work without a path variable (see *Scripts*) |
43
44
  | `hooks/` | enforcement at the moment of the action | runs on the user's machine, every matching tool call | a rule matters more than an instruction can guarantee |
44
45
  | `agents/` | a job done in a separate context window | ~100 always-on tokens per agent | the work would otherwise crowd out the main thread |
45
46
  | `commands/` | a user-typed entry point with arguments | ~50–100 always-on tokens | a human, not the model, decides when it runs |
@@ -154,19 +155,42 @@ carry the trigger phrases that reach the same behavior in plain language.
154
155
 
155
156
  The one accelerator that travels. Keep it inside the skill directory
156
157
  (`scripts/`), stdlib-only, and invoke it by a path the agent can actually
157
- resolve:
158
+ resolve.
159
+
160
+ **The trap that costs the most here: `${CLAUDE_PLUGIN_ROOT}` does not work in a
161
+ command you tell the agent to run.** It is substituted into skill, command and
162
+ agent *text*, and it is exported to *hook and monitor* processes — but it is
163
+ **not** in the Bash tool's environment. Measured on Claude Code 2.1.220:
158
164
 
159
165
  ```bash
160
- python3 "${CLAUDE_PLUGIN_ROOT}/skills/<skill>/scripts/x.py" <target> # Claude Code
161
- python3 ~/.agents/skills/<skill>/scripts/x.py <target> # skills CLI
162
- python3 ~/.claude/skills/<skill>/scripts/x.py <target> # plain install
166
+ $ echo "[${CLAUDE_PLUGIN_ROOT}]"
167
+ []
168
+ $ python3 "${CLAUDE_PLUGIN_ROOT}/skills/x/scripts/x.py" .
169
+ python3: can't open file '/skills/x/scripts/x.py': [Errno 2] No such file or directory
163
170
  ```
164
171
 
165
- **The absolute path differs per channel**, so the instruction that survives is
166
- "run the script from the skill directory you just read this file from", with
167
- `${CLAUDE_PLUGIN_ROOT}` offered as the Claude Code convenience. State the
168
- no-interpreter fallback too: the manual procedure, in the reference that the
169
- script implements.
172
+ That failure is expensive out of proportion to its size: the agent was told to
173
+ run a deterministic check, the check will not run, and reasoning through it
174
+ instead — then reporting the result as if it had run — is the cheapest way out.
175
+
176
+ **Ship a wrapper in the plugin's `bin/` instead.** Claude Code puts a plugin's
177
+ `bin/` on the Bash tool's PATH while the plugin is enabled, so the script
178
+ becomes reachable by name, with no variable in the command:
179
+
180
+ ```bash
181
+ <plugin>-<verb> <target> # Claude Code: bin/ is on PATH
182
+ ```
183
+
184
+ Make the wrapper resolve its payload from **its own location** (`dirname "$0"`,
185
+ following symlinks), never from an environment variable — then it also works
186
+ copied, symlinked into an agents hub, or called by absolute path. Worked
187
+ example: this plugin's `bin/make-skill-audit`.
188
+
189
+ Everywhere else the absolute path differs per channel
190
+ (`~/.agents/skills/<skill>/`, `~/.claude/skills/<skill>/`), so the instruction
191
+ that survives is "run the script from the skill directory you just read this
192
+ file from". State the no-interpreter fallback too: the manual procedure, in the
193
+ reference that the script implements, and the items it leaves **NOT-RUN**.
170
194
 
171
195
  ## MCP servers and other plugins as dependencies
172
196
 
@@ -215,4 +239,6 @@ what the agent reads at the exact moment something is missing.
215
239
  - [ ] No command named after a skill; every `argument-hint` quoted
216
240
  - [ ] Plugin agents carry no `hooks` / `mcpServers` / `permissionMode`
217
241
  - [ ] Scripts are stdlib-only, inside the skill dir, invoked by a resolvable path
242
+ - [ ] No command the agent is told to RUN contains `${CLAUDE_PLUGIN_ROOT}` — it is
243
+ empty in the Bash tool; ship a `bin/` wrapper and call it by name
218
244
  - [ ] MCP and sibling-skill dependencies declared in `compatibility` with a branch
@@ -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
@@ -30,6 +30,10 @@ import sys
30
30
  # --- limits, each with the authority that sets it --------------------------
31
31
  NAME_MAX = 64 # spec: name is 1-64 characters
32
32
  DESC_MAX = 1024 # spec: description is 1-1024 characters
33
+ # House working limit: 5% under the cap. The description is the whole triggering
34
+ # budget AND the field that must grow when a near-miss skill appears ("say what
35
+ # it is NOT for"). A description at 98% of cap cannot absorb that sentence.
36
+ DESC_TARGET = 970
33
37
  COMPAT_MAX = 500 # spec: compatibility is 1-500 characters
34
38
  BODY_MAX_LINES = 500 # spec + Anthropic: keep the body under 500 lines
35
39
  BODY_MAX_TOKENS = 5000 # spec + Anthropic: level-2 budget
@@ -171,7 +175,10 @@ def _check_name(a, fm, lines, dir_name, rel):
171
175
  a.gap("NAME_CHARSET", "name %r must be lowercase a-z0-9 with single internal "
172
176
  "hyphens — no uppercase, no leading/trailing or doubled hyphen" % name, rel, ln)
173
177
  else:
174
- a.ok("NAME_CHARSET", "name %r is spec-legal" % name, rel, ln)
178
+ # Charset only. Saying "spec-legal" here would contradict NAME_RESERVED
179
+ # and NAME_XML below, and a PASS line quoted out of context is exactly
180
+ # how a wrong verdict acquires real command output as its evidence.
181
+ a.ok("NAME_CHARSET", "name %r uses a legal charset" % name, rel, ln)
175
182
  if name != dir_name:
176
183
  a.gap("NAME_DIR", "name %r != directory %r — the spec requires them to match, "
177
184
  "and the Skills API matches the uploaded top-level directory against it"
@@ -211,10 +218,24 @@ def _check_description(a, fm, lines, rel, house):
211
218
  else:
212
219
  a.ok("DESC_PERSON", "description is third person", rel, ln)
213
220
  if house:
221
+ # Both report either way: "the house rules were checked" has to be
222
+ # provable from the output the canon tells the agent to cite.
214
223
  if not desc.lower().startswith("use when"):
215
224
  a.gap("DESC_USEWHEN", "description must start with 'Use when …' (house rule)", rel, ln)
225
+ else:
226
+ a.ok("DESC_USEWHEN", "description opens with 'Use when …' (house rule)", rel, ln)
216
227
  if not re.search(r"[а-яё]", desc, re.I):
217
228
  a.gap("DESC_RU", "description carries no Russian trigger phrases (house rule)", rel, ln)
229
+ else:
230
+ a.ok("DESC_RU", "description carries Russian trigger phrases (house rule)", rel, ln)
231
+ if DESC_TARGET < len(desc) <= DESC_MAX:
232
+ a.gap("DESC_HEADROOM", "description is %d chars — inside the %d cap but past the "
233
+ "%d working limit (house rule): leave room for the 'what this is NOT for' "
234
+ "clause a near-miss neighbour will require"
235
+ % (len(desc), DESC_MAX, DESC_TARGET), rel, ln)
236
+ elif len(desc) <= DESC_TARGET:
237
+ a.ok("DESC_HEADROOM", "description is %d/%d chars, inside the working limit"
238
+ % (len(desc), DESC_TARGET), rel, ln)
218
239
 
219
240
 
220
241
  def _check_optional_fields(a, fm, lines, rel):
@@ -253,13 +274,18 @@ def _check_keys(a, fm, lines, rel):
253
274
  def _check_body_budget(a, body, rel):
254
275
  n_lines = body.count("\n") + 1
255
276
  est = int(len(body) / CHARS_PER_TOKEN)
277
+ # Both, not either: a body over the line budget still has to report its
278
+ # token count, or the second fix arrives only after the first one ships.
279
+ over = False
256
280
  if n_lines >= BODY_MAX_LINES:
257
281
  a.gap("BODY_LINES", "body is %d lines, the budget is < %d — move detail into "
258
282
  "references/" % (n_lines, BODY_MAX_LINES), rel)
259
- elif est >= BODY_MAX_TOKENS:
283
+ over = True
284
+ if est >= BODY_MAX_TOKENS:
260
285
  a.gap("BODY_TOKENS", "body is ~%d tokens (%d chars / %s), the budget is < %d"
261
286
  % (est, len(body), CHARS_PER_TOKEN, BODY_MAX_TOKENS), rel)
262
- else:
287
+ over = True
288
+ if not over:
263
289
  a.ok("BODY_BUDGET", "body is %d lines / ~%d tokens (budget %d / %d)"
264
290
  % (n_lines, est, BODY_MAX_LINES, BODY_MAX_TOKENS), rel)
265
291
 
@@ -295,21 +321,27 @@ def _check_bundle(a, skill_dir, skill_text, dir_name):
295
321
 
296
322
 
297
323
  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:
324
+ """A relative link that escapes the skill directory arrives broken everywhere.
325
+
326
+ Reported per line: the canon defines evidence as a `file:line`, so a finding
327
+ without one forces the agent to either drop the evidence or invent it.
328
+ """
329
+ bad = False
330
+ for i, line in enumerate(skill_text.split("\n"), start=1):
331
+ for target in LINK_RE.findall(line):
332
+ if target.startswith(("http://", "https://", "mailto:", "#")):
333
+ continue
334
+ if target.startswith("../") or "/../" in target:
335
+ a.gap("LINK_ESCAPE", "link %r escapes the skill directory — packagers "
336
+ "ship that directory alone, so it arrives broken on every agent"
337
+ % target, rel, i)
338
+ bad = True
339
+ continue
340
+ path = os.path.normpath(os.path.join(skill_dir, target.split("#")[0]))
341
+ if not os.path.exists(path):
342
+ a.gap("LINK_BROKEN", "link %r does not resolve" % target, rel, i)
343
+ bad = True
344
+ if not bad:
313
345
  a.ok("LINK_INTEGRITY", "every relative link resolves and stays inside the skill", rel)
314
346
 
315
347
 
@@ -335,8 +367,11 @@ def _check_prose(a, body, rel):
335
367
  a.gap("TIME_BRANCH", "time-branching instruction (%r) — it is wrong the day "
336
368
  "it ships; put superseded material under '## Old patterns'"
337
369
  % 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):
370
+ # A pointer at a directory rather than a file with a load condition. Quoted
371
+ # spans are blanked here too: a canon that forbids "see references/" has to
372
+ # be able to quote the phrase it forbids.
373
+ for i, raw in enumerate(body.split("\n"), start=1):
374
+ line = _strip_quoted(raw)
340
375
  if re.search(r"\b(?:see|read)\s+`?(?:references|scripts|assets)/`?(?![\w.-])", line, re.I):
341
376
  a.gap("REF_NO_TRIGGER", "points at a directory instead of a file with a "
342
377
  "stated load condition — 'read X when Y' beats 'see references/'", rel, i)