skill-check 1.0.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 +6 -0
- package/dist/cli/main.js +13 -3
- package/dist/core/analyze.js +2 -0
- package/dist/core/conclusion-card.d.ts +1 -1
- package/dist/core/conclusion-card.js +2 -0
- package/dist/core/defaults.js +9 -1
- package/dist/core/discovery.js +0 -18
- package/dist/core/formatters.js +2 -0
- package/dist/rules/core/description.js +21 -0
- package/dist/rules/core/frontmatter.js +124 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +3 -1
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
|
@@ -605,15 +605,19 @@ async function runAgentScanWithFeedback(scanOptions, target, io, format) {
|
|
|
605
605
|
}
|
|
606
606
|
}
|
|
607
607
|
function resolveValidationStatus(result) {
|
|
608
|
+
if (result.summary.skillCount === 0)
|
|
609
|
+
return 'SKIPPED';
|
|
608
610
|
if (result.summary.errorCount > 0)
|
|
609
611
|
return 'FAIL';
|
|
610
612
|
if (result.summary.warningCount > 0)
|
|
611
613
|
return 'WARN';
|
|
612
614
|
return 'PASS';
|
|
613
615
|
}
|
|
614
|
-
function resolveSecurityStatus(enabled, scanExitCode) {
|
|
616
|
+
function resolveSecurityStatus(enabled, scanExitCode, skillCount) {
|
|
615
617
|
if (!enabled)
|
|
616
618
|
return 'SKIPPED';
|
|
619
|
+
if (skillCount === 0)
|
|
620
|
+
return 'SKIPPED';
|
|
617
621
|
return scanExitCode === 0 ? 'PASS' : 'FAIL';
|
|
618
622
|
}
|
|
619
623
|
function computeOverallScore(scores) {
|
|
@@ -848,7 +852,7 @@ export async function runCli(argv, io = defaultIO) {
|
|
|
848
852
|
affectedFileCount: countAffectedFiles(result.diagnostics),
|
|
849
853
|
overallScore: computeOverallScore(scores),
|
|
850
854
|
validationStatus: resolveValidationStatus(result),
|
|
851
|
-
securityStatus: resolveSecurityStatus(scanOptions.enabled, scanExitCode),
|
|
855
|
+
securityStatus: resolveSecurityStatus(scanOptions.enabled, scanExitCode, result.summary.skillCount),
|
|
852
856
|
elapsedMs: performance.now() - checkStartedAt,
|
|
853
857
|
runCommand,
|
|
854
858
|
mode: checkOptions.share ? 'share' : 'default',
|
|
@@ -857,7 +861,7 @@ export async function runCli(argv, io = defaultIO) {
|
|
|
857
861
|
if (conclusion.fullCommandPlain) {
|
|
858
862
|
io.stdout(`${conclusion.fullCommandPlain}\n`);
|
|
859
863
|
}
|
|
860
|
-
if (checkOptions.share) {
|
|
864
|
+
if (checkOptions.share && result.summary.skillCount > 0) {
|
|
861
865
|
emitShareStatus(io, 'rendering share image...');
|
|
862
866
|
const shareOutputPath = path.resolve(process.cwd(), checkOptions.shareOut ?? 'skill-check-share.png');
|
|
863
867
|
const shareText = conclusion.fullCommandPlain
|
|
@@ -1052,6 +1056,12 @@ export async function runCli(argv, io = defaultIO) {
|
|
|
1052
1056
|
'---',
|
|
1053
1057
|
`name: ${slug}`,
|
|
1054
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',
|
|
1055
1065
|
'---',
|
|
1056
1066
|
'',
|
|
1057
1067
|
`# ${slug}`,
|
package/dist/core/analyze.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ValidationStatus = 'PASS' | 'WARN' | 'FAIL';
|
|
1
|
+
export type ValidationStatus = 'PASS' | 'WARN' | 'FAIL' | 'SKIPPED';
|
|
2
2
|
export type SecurityStatus = 'PASS' | 'FAIL' | 'SKIPPED';
|
|
3
3
|
export type ConclusionCardMode = 'default' | 'share';
|
|
4
4
|
export interface ConclusionCardInput {
|
package/dist/core/defaults.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const DEFAULT_INCLUDE = ['**/
|
|
1
|
+
export const DEFAULT_INCLUDE = ['**/SKILL.md'];
|
|
2
2
|
export const DEFAULT_EXCLUDE = [
|
|
3
3
|
'**/node_modules/**',
|
|
4
4
|
'**/.git/**',
|
|
@@ -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',
|
package/dist/core/discovery.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fg from 'fast-glob';
|
|
4
|
-
import { DEFAULT_INCLUDE } from './defaults.js';
|
|
5
4
|
export async function discoverSkillFiles(config) {
|
|
6
5
|
const found = new Set();
|
|
7
|
-
const usesDefaultInclude = config.include.length === DEFAULT_INCLUDE.length &&
|
|
8
|
-
config.include.every((pattern, index) => pattern === DEFAULT_INCLUDE[index]);
|
|
9
6
|
for (const root of config.rootsAbs) {
|
|
10
7
|
if (fs.existsSync(root) && fs.statSync(root).isFile()) {
|
|
11
8
|
if (path.basename(root) === 'SKILL.md') {
|
|
@@ -23,21 +20,6 @@ export async function discoverSkillFiles(config) {
|
|
|
23
20
|
onlyFiles: true,
|
|
24
21
|
dot: true,
|
|
25
22
|
});
|
|
26
|
-
if (matches.length === 0 &&
|
|
27
|
-
usesDefaultInclude &&
|
|
28
|
-
path.basename(path.resolve(root)) === 'skills') {
|
|
29
|
-
const fallbackMatches = await fg('**/SKILL.md', {
|
|
30
|
-
cwd: root,
|
|
31
|
-
ignore: config.exclude,
|
|
32
|
-
absolute: true,
|
|
33
|
-
onlyFiles: true,
|
|
34
|
-
dot: true,
|
|
35
|
-
});
|
|
36
|
-
for (const match of fallbackMatches) {
|
|
37
|
-
found.add(path.resolve(match));
|
|
38
|
-
}
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
23
|
for (const match of matches) {
|
|
42
24
|
found.add(path.resolve(match));
|
|
43
25
|
}
|
package/dist/core/formatters.js
CHANGED
|
@@ -84,6 +84,8 @@ function renderScoreBar(score) {
|
|
|
84
84
|
return `${color('█'.repeat(filled))}${pc.dim('░'.repeat(empty))} ${color(String(score))}`;
|
|
85
85
|
}
|
|
86
86
|
function resolveValidationStatus(result) {
|
|
87
|
+
if (result.summary.skillCount === 0)
|
|
88
|
+
return 'SKIPPED';
|
|
87
89
|
if (result.summary.errorCount > 0)
|
|
88
90
|
return 'FAIL';
|
|
89
91
|
if (result.summary.warningCount > 0)
|
|
@@ -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
package/package.json
CHANGED
|
@@ -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
|
},
|