ultra11y 2.25.0 → 2.26.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.
@@ -15,9 +15,47 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
15
15
  import { dirname, join } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
 
18
- // Claude Code matches skill descriptions at <=1024 chars; 1000 leaves a safety
19
- // margin so a future edit can't silently cross the cap.
20
- const DESC_MAX = 1000;
18
+ // Claude Code truncates the COMBINED `description` + `when_to_use` text at 1536
19
+ // chars in the skill listing — that listing is what the model matches a task
20
+ // against, so anything past the cut is invisible and the skill silently stops
21
+ // auto-invoking. 1500 leaves a safety margin. NOTE: the budget is shared, not
22
+ // per-field: growing `description` eats into `when_to_use` and vice versa.
23
+ const LISTING_MAX = 1500;
24
+
25
+ // Frontmatter keys Claude Code understands, plus the two this repo carries
26
+ // (`license`, `metadata`). An unknown key at the root is almost always a typo
27
+ // — `when-to-use` for `when_to_use` costs the whole automatic trigger and
28
+ // otherwise fails silently, so it is a hard error here.
29
+ const KNOWN_KEYS = new Set([
30
+ "name",
31
+ "description",
32
+ "when_to_use",
33
+ "argument-hint",
34
+ "arguments",
35
+ "disable-model-invocation",
36
+ "user-invocable",
37
+ "allowed-tools",
38
+ "disallowed-tools",
39
+ "model",
40
+ "effort",
41
+ "context",
42
+ "agent",
43
+ "background",
44
+ "hooks",
45
+ "paths",
46
+ "shell",
47
+ "license",
48
+ "metadata",
49
+ ]);
50
+
51
+ /** A single-line, optionally quoted frontmatter scalar. The frontmatter here is
52
+ * deliberately kept single-line-per-key (no folded `>-` blocks) so this regex
53
+ * read stays honest — `sync-version.mjs` relies on the same shape. */
54
+ const scalar = (fm, key) =>
55
+ fm
56
+ .match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]
57
+ ?.trim()
58
+ .replace(/^["']|["']$/g, "") ?? null;
21
59
 
22
60
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
23
61
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
@@ -55,13 +93,24 @@ for (const name of skillNames) {
55
93
  if (!fm) bad(`skills/${name}/SKILL.md has no frontmatter block`);
56
94
  else {
57
95
  ok("packaged SKILL.md present with frontmatter");
58
- const nameLine = fm[1].match(/^name:\s*(.+)$/m)?.[1]?.trim();
96
+ const nameLine = scalar(fm[1], "name");
59
97
  nameLine === name ? ok(`frontmatter name "${name}" matches its directory`) : bad(`frontmatter name "${nameLine}" != skill directory "${name}"`);
60
- const desc = fm[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
98
+
99
+ // Only root-level keys: `metadata:`'s children are indented, so ^ under /m skips them.
100
+ const unknown = [...fm[1].matchAll(/^([A-Za-z][\w-]*):/gm)].map((m) => m[1]).filter((k) => !KNOWN_KEYS.has(k));
101
+ unknown.length === 0
102
+ ? ok("frontmatter keys are all recognised")
103
+ : bad(`skills/${name}: unknown frontmatter key(s) ${unknown.map((k) => `"${k}"`).join(", ")} — a typo here fails silently (e.g. "when-to-use" instead of "when_to_use")`);
104
+
105
+ const desc = scalar(fm[1], "description");
106
+ const when = scalar(fm[1], "when_to_use");
61
107
  if (!desc) bad(`skills/${name}: frontmatter has no description`);
62
108
  else {
63
- const len = desc.replace(/^["']|["']$/g, "").length;
64
- len <= DESC_MAX ? ok(`description ${len} chars (<= ${DESC_MAX} safety cap)`) : bad(`skills/${name}: description ${len} chars exceeds the ${DESC_MAX}-char safety cap (matcher limit is 1024)`);
109
+ const total = desc.length + (when?.length ?? 0);
110
+ const parts = `description ${desc.length}${when ? ` + when_to_use ${when.length}` : ""} = ${total}`;
111
+ total <= LISTING_MAX
112
+ ? ok(`${parts} chars (<= ${LISTING_MAX} safety cap)`)
113
+ : bad(`skills/${name}: ${parts} chars exceeds the ${LISTING_MAX}-char safety cap — Claude Code truncates the listing at 1536 and the tail stops matching`);
65
114
  }
66
115
  }
67
116
 
@@ -84,6 +133,54 @@ for (const name of skillNames) {
84
133
  : bad(`skills/${name}/${engine} differs from ${engine} — run \`node scripts/copy-bundle.mjs\` and commit`);
85
134
  }
86
135
 
136
+ // 5. The Claude Code PLUGIN shape, which is what carries the hooks. `skills add`
137
+ // installs skills only; the automatic pre-PR review needs the plugin manifests
138
+ // and the hook guard to ship together, so their absence is a hard failure.
139
+ console.log("plugin:");
140
+ const readJson = (rel) => {
141
+ const p = join(root, rel);
142
+ if (!existsSync(p)) {
143
+ bad(`missing ${rel} — the plugin would not install (no hooks, so no automatic review)`);
144
+ return null;
145
+ }
146
+ try {
147
+ return JSON.parse(readFileSync(p, "utf8"));
148
+ } catch (e) {
149
+ bad(`${rel} is not valid JSON: ${e.message}`);
150
+ return null;
151
+ }
152
+ };
153
+
154
+ const plugin = readJson(".claude-plugin/plugin.json");
155
+ if (plugin) {
156
+ plugin.name === pkg.name ? ok(`plugin.json name "${plugin.name}" matches the package`) : bad(`.claude-plugin/plugin.json name "${plugin.name}" != package name "${pkg.name}"`);
157
+ plugin.version === pkg.version
158
+ ? ok(`plugin.json version ${plugin.version} matches the package`)
159
+ : bad(`.claude-plugin/plugin.json version "${plugin.version}" != package version "${pkg.version}" — add it to scripts/sync-version.mjs`);
160
+ plugin.description ? ok("plugin.json has a description") : bad(".claude-plugin/plugin.json has no description (required)");
161
+ }
162
+
163
+ const marketplace = readJson(".claude-plugin/marketplace.json");
164
+ if (marketplace) {
165
+ Array.isArray(marketplace.plugins) && marketplace.plugins.some((p) => p.name === pkg.name)
166
+ ? ok(`marketplace.json lists the "${pkg.name}" plugin`)
167
+ : bad(`.claude-plugin/marketplace.json does not list a plugin named "${pkg.name}"`);
168
+ }
169
+
170
+ const hooksJson = readJson("hooks/hooks.json");
171
+ if (hooksJson) {
172
+ Array.isArray(hooksJson.hooks?.PreToolUse) && hooksJson.hooks.PreToolUse.length > 0
173
+ ? ok("hooks.json declares a PreToolUse hook")
174
+ : bad("hooks/hooks.json declares no PreToolUse hook — nothing would trigger the review before a commit/push/PR");
175
+ }
176
+
177
+ existsSync(join(root, "hooks/pre-tool-use.mjs"))
178
+ ? ok("hooks/pre-tool-use.mjs present")
179
+ : bad("missing hooks/pre-tool-use.mjs — hooks.json points at a guard that does not exist");
180
+
181
+ for (const f of [".claude-plugin", "hooks"])
182
+ (pkg.files ?? []).includes(f) ? ok(`package.json files[] carries ${f}/`) : bad(`package.json "files" is missing "${f}" — the npm tarball would ship without it`);
183
+
87
184
  if (errors.length) {
88
185
  console.error(`\nverify-skill-bundle: ${errors.length} problem(s) — a published skill would not install correctly.`);
89
186
  process.exit(1);
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  name: review-a11y
3
3
  description: "Use to REVIEW code changes for accessibility — '/review-a11y', 'review my changes/PR/branch for a11y', 'accessibility review of this diff'. Scopes to exactly the code under change (staged files, working diff, or branch vs merge-base), runs the bundled install-free ultra11y engine (node scripts/ultra11y.mjs, no keys; static WCAG 2.2 AA checks tied to success criteria, cross-file JSX/TSX via --graph), then adds YOUR judgment: refute false positives (SFC/library-source findings are preliminary), decide alt relevance and link purpose, name the rendering criteria (contrast, focus, zoom) as residual risks — never invent a non-conformity. Output: a severity-ranked review (blocking → major → minor) with file:line, concrete fixes, residual risks and a one-line verdict, in the conversation's language (technical tokens stay in English). Companion of the ultra11y skill (full audits, reports, PRD, RGAA packs). Triggers: 'review a11y', 'a11y review', 'accessibility review'."
4
+ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should be checked: the user just finished or modified a component, template, page or stylesheet; is about to commit, push or open a pull request; asks 'is this accessible?' or 'anything to fix before merge?'; a diff, branch or PR is under discussion; or a hook or CI reports accessibility findings on the change. NOT for a whole-repo audit, a dated conformance report, an RGAA/Section 508 deliverable or a PRD backlog — that is the `ultra11y` skill."
5
+ allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
4
6
  license: MIT
5
7
  metadata:
6
- version: 2.25.0
8
+ version: 2.26.0
7
9
  ---
8
10
 
9
11
  # review-a11y — accessibility review of changed code
@@ -112,8 +114,12 @@ rendered DOM; residual risks listed above.
112
114
  - **Formal deliverables** (dated report, auditor conformance blocks, PRD backlog, GitHub
113
115
  issues, RGAA/country-standard vocabulary): that is the `ultra11y` skill — same engine,
114
116
  same findings (`report`, `prd`, `check`/`verify` gates).
115
- - **Make it automatic**: the `ultra11y` skill's `init --hook` installs a pre-commit gate
116
- running this same staged review mechanically.
117
+ - **Make it automatic**: installed as a **Claude Code plugin**, a `PreToolUse` hook stops a
118
+ pending `git commit` / `git push` / `gh pr create` that carries findings and hands them
119
+ over — that is what invokes this skill without anyone asking. It never fires twice for the
120
+ same findings, so a retry lands. For a gate that does not need an agent at all, the
121
+ `ultra11y` skill's `init --hook` installs a mechanical pre-commit run of this same staged
122
+ audit.
117
123
 
118
124
  ## Do not
119
125