skill-check 1.1.0 → 1.2.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/README.md CHANGED
@@ -382,7 +382,13 @@ Run `skill-check rules <id>` for detail on a specific rule.
382
382
  | `frontmatter.description_required` | error | yes |
383
383
  | `frontmatter.name_matches_directory` | error | yes |
384
384
  | `frontmatter.name_slug_format` | error | yes |
385
+ | `frontmatter.name_max_length` | error | no |
385
386
  | `frontmatter.field_order` | error | yes |
387
+ | `frontmatter.unknown_fields` | warn | no |
388
+ | `frontmatter.compatibility_max_length` | warn | no |
389
+ | `frontmatter.metadata_string_values` | warn | no |
390
+ | `frontmatter.allowed_tools_format` | warn | no |
391
+ | `description.non_empty` | error | no |
386
392
  | `description.max_length` | error | no |
387
393
  | `description.use_when_phrase` | warn | yes |
388
394
  | `description.min_recommended_length` | warn | yes |
package/dist/cli/main.js CHANGED
@@ -1056,6 +1056,12 @@ export async function runCli(argv, io = defaultIO) {
1056
1056
  '---',
1057
1057
  `name: ${slug}`,
1058
1058
  `description: Use when ${slug} functionality is needed. Describe triggers, constraints, and expected outcomes.`,
1059
+ '# license: MIT',
1060
+ '# compatibility: Designed for Claude Code (or similar products)',
1061
+ '# metadata:',
1062
+ '# author: your-org',
1063
+ '# version: "1.0"',
1064
+ '# allowed-tools: Bash(git:*) Read',
1059
1065
  '---',
1060
1066
  '',
1061
1067
  `# ${slug}`,
@@ -58,6 +58,8 @@ export function ensureInitConfig(targetPath, force = false) {
58
58
  maxBodyLines: 500,
59
59
  minDescriptionChars: 50,
60
60
  maxBodyTokens: 5000,
61
+ maxNameChars: 64,
62
+ maxCompatibilityChars: 500,
61
63
  },
62
64
  rules: {
63
65
  'description.use_when_phrase': 'warn',
@@ -13,6 +13,8 @@ export const DEFAULT_LIMITS = {
13
13
  maxBodyLines: 500,
14
14
  minDescriptionChars: 50,
15
15
  maxBodyTokens: 5000,
16
+ maxNameChars: 64,
17
+ maxCompatibilityChars: 500,
16
18
  };
17
19
  export const DEFAULT_OUTPUT = {
18
20
  format: 'text',
@@ -23,8 +25,14 @@ export const DEFAULT_RULE_LEVELS = {
23
25
  'frontmatter.description_required': 'error',
24
26
  'frontmatter.name_matches_directory': 'error',
25
27
  'frontmatter.name_slug_format': 'error',
28
+ 'frontmatter.name_max_length': 'error',
26
29
  'frontmatter.field_order': 'error',
30
+ 'frontmatter.unknown_fields': 'warn',
31
+ 'frontmatter.compatibility_max_length': 'warn',
32
+ 'frontmatter.metadata_string_values': 'warn',
33
+ 'frontmatter.allowed_tools_format': 'warn',
27
34
  'description.max_length': 'error',
35
+ 'description.non_empty': 'error',
28
36
  'description.use_when_phrase': 'warn',
29
37
  'description.min_recommended_length': 'warn',
30
38
  'body.max_lines': 'error',
@@ -7,6 +7,27 @@ function getDescription(skill) {
7
7
  return value.trim();
8
8
  }
9
9
  export const descriptionRules = [
10
+ {
11
+ id: 'description.non_empty',
12
+ description: 'Description must not be empty or whitespace-only.',
13
+ defaultSeverity: 'error',
14
+ evaluate(skill) {
15
+ if (!skill.frontmatter)
16
+ return [];
17
+ const raw = skill.frontmatter.description;
18
+ if (raw === undefined || raw === null)
19
+ return [];
20
+ const trimmed = typeof raw === 'string' ? raw.trim() : String(raw).trim();
21
+ if (trimmed.length > 0)
22
+ return [];
23
+ return [
24
+ {
25
+ message: 'description is empty or whitespace-only',
26
+ suggestion: 'Provide a meaningful description. Start with "Use when" to help agents match this skill.',
27
+ },
28
+ ];
29
+ },
30
+ },
10
31
  {
11
32
  id: 'description.max_length',
12
33
  description: 'Description must be within configured max length.',
@@ -3,6 +3,14 @@ function normalizeName(name) {
3
3
  return '';
4
4
  return name.replace(/^['"]|['"]$/g, '').trim();
5
5
  }
6
+ const KNOWN_FRONTMATTER_FIELDS = new Set([
7
+ 'name',
8
+ 'description',
9
+ 'license',
10
+ 'compatibility',
11
+ 'metadata',
12
+ 'allowed-tools',
13
+ ]);
6
14
  export const frontmatterRules = [
7
15
  {
8
16
  id: 'frontmatter.required',
@@ -100,6 +108,27 @@ export const frontmatterRules = [
100
108
  ];
101
109
  },
102
110
  },
111
+ {
112
+ id: 'frontmatter.name_max_length',
113
+ description: 'Frontmatter name must not exceed 64 characters.',
114
+ defaultSeverity: 'error',
115
+ evaluate(skill, context) {
116
+ if (!skill.frontmatter)
117
+ return [];
118
+ const name = normalizeName(skill.frontmatter.name);
119
+ if (!name)
120
+ return [];
121
+ const max = context.config.limits.maxNameChars;
122
+ if (name.length <= max)
123
+ return [];
124
+ return [
125
+ {
126
+ message: `name length ${name.length} exceeds max ${max}`,
127
+ suggestion: `Shorten the name to ${max} characters or fewer.`,
128
+ },
129
+ ];
130
+ },
131
+ },
103
132
  {
104
133
  id: 'frontmatter.field_order',
105
134
  description: 'Frontmatter should list name before description.',
@@ -121,4 +150,99 @@ export const frontmatterRules = [
121
150
  ];
122
151
  },
123
152
  },
153
+ {
154
+ id: 'frontmatter.unknown_fields',
155
+ description: 'Frontmatter should only contain spec-defined fields (name, description, license, compatibility, metadata, allowed-tools).',
156
+ defaultSeverity: 'warn',
157
+ evaluate(skill) {
158
+ if (!skill.frontmatter)
159
+ return [];
160
+ const unknown = Object.keys(skill.frontmatter).filter((key) => !KNOWN_FRONTMATTER_FIELDS.has(key));
161
+ if (unknown.length === 0)
162
+ return [];
163
+ const plural = unknown.length === 1 ? 'field' : 'fields';
164
+ return [
165
+ {
166
+ message: `unknown frontmatter ${plural}: ${unknown.join(', ')}`,
167
+ suggestion: `The spec defines: ${[...KNOWN_FRONTMATTER_FIELDS].join(', ')}. Remove or rename unrecognized fields.`,
168
+ },
169
+ ];
170
+ },
171
+ },
172
+ {
173
+ id: 'frontmatter.compatibility_max_length',
174
+ description: 'Compatibility field must not exceed 500 characters when provided.',
175
+ defaultSeverity: 'warn',
176
+ evaluate(skill, context) {
177
+ if (!skill.frontmatter)
178
+ return [];
179
+ const value = skill.frontmatter.compatibility;
180
+ if (value === undefined || value === null)
181
+ return [];
182
+ if (typeof value !== 'string')
183
+ return [];
184
+ const max = context.config.limits.maxCompatibilityChars;
185
+ if (value.trim().length <= max)
186
+ return [];
187
+ return [
188
+ {
189
+ message: `compatibility length ${value.trim().length} exceeds max ${max}`,
190
+ suggestion: `Shorten the compatibility field to ${max} characters or fewer.`,
191
+ },
192
+ ];
193
+ },
194
+ },
195
+ {
196
+ id: 'frontmatter.metadata_string_values',
197
+ description: 'Metadata field must be a map of string keys to string values.',
198
+ defaultSeverity: 'warn',
199
+ evaluate(skill) {
200
+ if (!skill.frontmatter)
201
+ return [];
202
+ const metadata = skill.frontmatter.metadata;
203
+ if (metadata === undefined || metadata === null)
204
+ return [];
205
+ if (typeof metadata !== 'object' || Array.isArray(metadata)) {
206
+ return [
207
+ {
208
+ message: 'metadata must be a key-value object',
209
+ suggestion: 'Use metadata as a YAML mapping, e.g. metadata:\\n author: example-org',
210
+ },
211
+ ];
212
+ }
213
+ const nonStringKeys = [];
214
+ for (const [key, val] of Object.entries(metadata)) {
215
+ if (typeof val !== 'string')
216
+ nonStringKeys.push(key);
217
+ }
218
+ if (nonStringKeys.length === 0)
219
+ return [];
220
+ return [
221
+ {
222
+ message: `metadata values must be strings; non-string keys: ${nonStringKeys.join(', ')}`,
223
+ suggestion: 'Convert metadata values to strings, e.g. version: "1.0" instead of version: 1.0',
224
+ },
225
+ ];
226
+ },
227
+ },
228
+ {
229
+ id: 'frontmatter.allowed_tools_format',
230
+ description: 'allowed-tools field must be a space-delimited string.',
231
+ defaultSeverity: 'warn',
232
+ evaluate(skill) {
233
+ if (!skill.frontmatter)
234
+ return [];
235
+ const value = skill.frontmatter['allowed-tools'];
236
+ if (value === undefined || value === null)
237
+ return [];
238
+ if (typeof value === 'string')
239
+ return [];
240
+ return [
241
+ {
242
+ message: 'allowed-tools must be a string (space-delimited tool list)',
243
+ suggestion: 'Use a space-delimited string, e.g. allowed-tools: Bash(git:*) Read',
244
+ },
245
+ ];
246
+ },
247
+ },
124
248
  ];
package/dist/types.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface LimitsConfig {
22
22
  maxBodyLines: number;
23
23
  minDescriptionChars: number;
24
24
  maxBodyTokens: number;
25
+ maxNameChars: number;
26
+ maxCompatibilityChars: number;
25
27
  }
26
28
  export interface OutputConfig {
27
29
  format: OutputFormat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-check",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Linter for agent skill files",
5
5
  "type": "module",
6
6
  "private": false,
@@ -22,7 +22,9 @@
22
22
  "maxDescriptionChars": { "type": "integer", "minimum": 1 },
23
23
  "maxBodyLines": { "type": "integer", "minimum": 1 },
24
24
  "minDescriptionChars": { "type": "integer", "minimum": 0 },
25
- "maxBodyTokens": { "type": "integer", "minimum": 1 }
25
+ "maxBodyTokens": { "type": "integer", "minimum": 1 },
26
+ "maxNameChars": { "type": "integer", "minimum": 1 },
27
+ "maxCompatibilityChars": { "type": "integer", "minimum": 1 }
26
28
  },
27
29
  "additionalProperties": false
28
30
  },