skills-doctor 0.2.0 → 0.3.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.
- package/CHANGELOG.md +23 -0
- package/README.md +4 -3
- package/bin/skills-doctor.js +2 -1
- package/dist/cli/commands/scan.d.ts.map +1 -1
- package/dist/cli/commands/scan.js +117 -39
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/utils/handoff-to-agent.js +14 -6
- package/dist/domain/rules/quality.js +51 -8
- package/dist/domain/scan-skills.d.ts.map +1 -1
- package/dist/domain/scan-skills.js +1 -2
- package/dist/domain/types.d.ts +1 -1
- package/dist/domain/types.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
8
8
|
|
|
9
9
|
*No changes yet.*
|
|
10
10
|
|
|
11
|
+
## [0.3.1] - 2026-06-16
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added line numbers to quality-rule findings where a specific source line can be resolved.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Hid repair handoff subset options that do not match any findings (for warning-only and advice-only scans).
|
|
20
|
+
- Made CLI module import safe by removing side-effect execution and routing runtime entry through `bin/skills-doctor.js`.
|
|
21
|
+
|
|
22
|
+
## [0.3.0] - 2026-06-16
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Kept the interactive review menu available after viewing grouped and/or error findings so users can still launch repair in the same session.
|
|
27
|
+
- Added support for selecting a custom skills directory during interactive scans even when standard Claude/Codex roots are already detected.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Ignored direct skill-root child directories that do not contain `SKILL.md` instead of reporting a blocking `missing-skill` finding.
|
|
32
|
+
- Classified missing referenced assets under the `assets` finding category instead of falling back to another resource category.
|
|
33
|
+
|
|
11
34
|
## [0.2.0] - 2026-06-16
|
|
12
35
|
|
|
13
36
|
### Added
|
package/README.md
CHANGED
|
@@ -57,9 +57,10 @@ It also detects these global user-level roots:
|
|
|
57
57
|
When local and global roots both exist, the interactive CLI first asks whether
|
|
58
58
|
to scan local project skills, global/root skills, or both. When both Claude and
|
|
59
59
|
Codex/agents roots exist in the selected scope, it asks whether to scan Claude,
|
|
60
|
-
Codex/agents, or both.
|
|
61
|
-
|
|
62
|
-
user error when a required
|
|
60
|
+
Codex/agents, or both. If you already have standard roots detected, it also lets
|
|
61
|
+
you add a custom skills directory path in the same interactive flow. Non-interactive
|
|
62
|
+
runs use conservative defaults and fail with a clear user error when a required
|
|
63
|
+
choice cannot be made.
|
|
63
64
|
|
|
64
65
|
## What It Checks
|
|
65
66
|
|
package/bin/skills-doctor.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAU/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM5F,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGhF,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEzE,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IACzD,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;CAC5C,CAAC;AAMF,eAAO,MAAM,UAAU,GACrB,WAAW,MAAM,EACjB,OAAO,SAAS,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAU/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM5F,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGhF,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEzE,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IACzD,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;CAC5C,CAAC;AAMF,eAAO,MAAM,UAAU,GACrB,WAAW,MAAM,EACjB,OAAO,SAAS,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,UAAU,CAsGpB,CAAC"}
|
|
@@ -39,17 +39,26 @@ export const scanAction = async (directory, flags, options = {}) => {
|
|
|
39
39
|
if (skipPrompts) {
|
|
40
40
|
throw new CliInputError("No .claude/skills or .agents/skills root was found. Re-run interactively or add a supported skills root.");
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const custom = await discoverSkillRoots({
|
|
42
|
+
roots = await selectCustomRoot({
|
|
44
43
|
cwd,
|
|
45
44
|
homeDir: options.homeDir,
|
|
46
|
-
|
|
45
|
+
prompts,
|
|
46
|
+
roots,
|
|
47
47
|
});
|
|
48
|
-
roots = custom.roots;
|
|
49
48
|
}
|
|
50
49
|
else if (!skipPrompts) {
|
|
51
|
-
roots = await selectRootScopes(
|
|
52
|
-
|
|
50
|
+
roots = await selectRootScopes({
|
|
51
|
+
roots,
|
|
52
|
+
prompts,
|
|
53
|
+
cwd,
|
|
54
|
+
homeDir: options.homeDir,
|
|
55
|
+
});
|
|
56
|
+
roots = await selectRoots({
|
|
57
|
+
roots,
|
|
58
|
+
prompts,
|
|
59
|
+
cwd,
|
|
60
|
+
homeDir: options.homeDir,
|
|
61
|
+
});
|
|
53
62
|
}
|
|
54
63
|
if (roots.length === 0) {
|
|
55
64
|
throw new CliInputError("No readable skills root was selected.");
|
|
@@ -96,57 +105,126 @@ export const scanAction = async (directory, flags, options = {}) => {
|
|
|
96
105
|
process.exitCode = resolveScanExitCode(finalReport);
|
|
97
106
|
return finalReport;
|
|
98
107
|
};
|
|
99
|
-
const selectRoots = async (
|
|
100
|
-
const
|
|
101
|
-
const
|
|
108
|
+
const selectRoots = async (input) => {
|
|
109
|
+
const { prompts, cwd, homeDir, roots } = input;
|
|
110
|
+
const customRoots = roots.filter((root) => root.source === "custom");
|
|
111
|
+
const standardRoots = roots.filter((root) => root.source !== "custom");
|
|
112
|
+
const hasClaude = standardRoots.some((root) => root.ecosystem === "claude");
|
|
113
|
+
const hasCodex = standardRoots.some((root) => root.ecosystem === "codex");
|
|
102
114
|
if (!hasClaude || !hasCodex)
|
|
103
115
|
return roots;
|
|
104
116
|
const selection = await prompts.select("Choose skills folder to scan", [
|
|
105
117
|
{ name: "Both", value: "all" },
|
|
106
118
|
{ name: "Claude (.claude/skills)", value: "claude" },
|
|
107
119
|
{ name: "Codex/agents (.agents/skills)", value: "codex" },
|
|
120
|
+
{ name: "Add custom skills path", value: "custom" },
|
|
108
121
|
]);
|
|
109
122
|
if (selection === "all")
|
|
110
123
|
return roots;
|
|
111
|
-
|
|
124
|
+
if (selection === "custom") {
|
|
125
|
+
return selectCustomRoot({
|
|
126
|
+
cwd,
|
|
127
|
+
homeDir,
|
|
128
|
+
prompts,
|
|
129
|
+
roots,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return [...standardRoots.filter((root) => root.ecosystem === selection), ...customRoots];
|
|
112
133
|
};
|
|
113
|
-
const selectRootScopes = async (
|
|
134
|
+
const selectRootScopes = async (input) => {
|
|
135
|
+
const { prompts, cwd, homeDir, roots } = input;
|
|
114
136
|
const hasLocal = roots.some((root) => root.source === "local");
|
|
115
137
|
const hasGlobal = roots.some((root) => root.source === "global");
|
|
116
|
-
|
|
138
|
+
const hasBothScopes = hasLocal && hasGlobal;
|
|
139
|
+
if (!hasLocal && !hasGlobal)
|
|
117
140
|
return roots;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
const allLabel = hasBothScopes
|
|
142
|
+
? "Both local project and global/root skills"
|
|
143
|
+
: "Detected skills root";
|
|
144
|
+
const choices = [
|
|
145
|
+
{ name: allLabel, value: "all" },
|
|
146
|
+
];
|
|
147
|
+
if (hasBothScopes) {
|
|
148
|
+
choices.push({
|
|
149
|
+
name: "Local project skills (./.claude/skills, ./.agents/skills)",
|
|
150
|
+
value: "local",
|
|
151
|
+
});
|
|
152
|
+
choices.push({
|
|
153
|
+
name: "Global/root skills (~/.claude/skills, ~/.agents/skills)",
|
|
154
|
+
value: "global",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
choices.push({ name: "Add custom skills path", value: "custom" });
|
|
158
|
+
if (choices.length <= 1) {
|
|
159
|
+
return roots;
|
|
160
|
+
}
|
|
161
|
+
const selection = await prompts.select("Choose skills scope to scan", choices);
|
|
162
|
+
if (selection === "custom") {
|
|
163
|
+
return selectCustomRoot({
|
|
164
|
+
cwd,
|
|
165
|
+
homeDir,
|
|
166
|
+
prompts,
|
|
167
|
+
roots,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
123
170
|
if (selection === "all")
|
|
124
171
|
return roots;
|
|
125
|
-
|
|
172
|
+
if (selection === "claude") {
|
|
173
|
+
return roots.filter((root) => root.ecosystem === "claude");
|
|
174
|
+
}
|
|
175
|
+
if (selection === "codex") {
|
|
176
|
+
return roots.filter((root) => root.ecosystem === "codex");
|
|
177
|
+
}
|
|
178
|
+
if (selection === "local" || selection === "global") {
|
|
179
|
+
return roots.filter((root) => root.source === selection);
|
|
180
|
+
}
|
|
181
|
+
return roots;
|
|
182
|
+
};
|
|
183
|
+
const selectCustomRoot = async (input) => {
|
|
184
|
+
const customRoot = await input.prompts.input("Skills directory path", ".");
|
|
185
|
+
const custom = await discoverSkillRoots({
|
|
186
|
+
cwd: input.cwd,
|
|
187
|
+
homeDir: input.homeDir,
|
|
188
|
+
customRoots: [{ rootPath: customRoot, ecosystem: "custom" }],
|
|
189
|
+
});
|
|
190
|
+
return mergeRoots(input.roots, custom.roots);
|
|
191
|
+
};
|
|
192
|
+
const mergeRoots = (existingRoots, additionalRoots) => {
|
|
193
|
+
const merged = new Map();
|
|
194
|
+
for (const root of existingRoots) {
|
|
195
|
+
merged.set(root.rootPath, root);
|
|
196
|
+
}
|
|
197
|
+
for (const root of additionalRoots) {
|
|
198
|
+
if (!merged.has(root.rootPath)) {
|
|
199
|
+
merged.set(root.rootPath, root);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [...merged.values()];
|
|
126
203
|
};
|
|
127
204
|
const reviewFindings = async (report, input) => {
|
|
128
205
|
const { prompts, write } = input;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
206
|
+
while (true) {
|
|
207
|
+
const action = await prompts.select("Next step", [
|
|
208
|
+
{ name: "Fix skills with Claude or Codex", value: "repair" },
|
|
209
|
+
...(report.errorCount > 0 ? [{ name: "View errors", value: "errors" }] : []),
|
|
210
|
+
{ name: "View all findings", value: "all" },
|
|
211
|
+
{ name: "View findings by skill", value: "by-skill" },
|
|
212
|
+
{ name: "Exit", value: "exit" },
|
|
213
|
+
]);
|
|
214
|
+
if (action === "exit")
|
|
215
|
+
return;
|
|
216
|
+
if (action === "repair") {
|
|
217
|
+
return runRepairAgentFlow(report, input);
|
|
218
|
+
}
|
|
219
|
+
const selectedFindings = action === "errors"
|
|
220
|
+
? report.findings.filter((finding) => finding.severity === "error")
|
|
221
|
+
: report.findings;
|
|
222
|
+
if (action === "by-skill") {
|
|
223
|
+
write(renderFindingsBySkill(selectedFindings));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
write(renderFindings(selectedFindings));
|
|
227
|
+
}
|
|
150
228
|
};
|
|
151
229
|
const runRepairAgentFlow = async (report, input) => {
|
|
152
230
|
try {
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
export declare const buildProgram: () => Command;
|
|
3
3
|
export declare const main: (argv?: readonly string[]) => Promise<void>;
|
|
4
|
+
export declare const runCli: (argv?: readonly string[]) => Promise<void>;
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,YAAY,QAAO,OAmB/B,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAAiB,KAAG,OAAO,CAAC,IAAI,CAY/E,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,YAAY,QAAO,OAmB/B,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAAiB,KAAG,OAAO,CAAC,IAAI,CAY/E,CAAC;AAEF,eAAO,MAAM,MAAM,GAAU,OAAM,SAAS,MAAM,EAAiB,KAAG,OAAO,CAAC,IAAI,CAEjF,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -34,12 +34,20 @@ export const prepareRepairHandoff = async (input) => {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
const chooseRepairFindings = async (report, prompts) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
{ name: "Blocking errors
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const choices = [];
|
|
38
|
+
if (report.errorCount > 0) {
|
|
39
|
+
choices.push({ name: "Blocking errors only", value: "errors" });
|
|
40
|
+
}
|
|
41
|
+
if (report.errorCount + report.warningCount > 0) {
|
|
42
|
+
choices.push({ name: "Blocking errors and warnings", value: "errors-and-warnings" });
|
|
43
|
+
}
|
|
44
|
+
if (report.findingCount > 0) {
|
|
45
|
+
choices.push({ name: "All findings", value: "all" });
|
|
46
|
+
}
|
|
47
|
+
if (report.skills.some((skill) => skill.findingCount > 0)) {
|
|
48
|
+
choices.push({ name: "Selected skills", value: "selected-skills" });
|
|
49
|
+
}
|
|
50
|
+
const subset = await prompts.select("Choose findings to repair", choices);
|
|
43
51
|
if (subset === "errors") {
|
|
44
52
|
return report.findings.filter((finding) => finding.severity === "error");
|
|
45
53
|
}
|
|
@@ -23,17 +23,19 @@ const validateSkillQuality = async (skill) => {
|
|
|
23
23
|
const findings = [];
|
|
24
24
|
const frontmatter = skill.parseResult.frontmatter;
|
|
25
25
|
const description = readString(frontmatter.data.description) ?? "";
|
|
26
|
-
const body = frontmatter.body
|
|
26
|
+
const body = frontmatter.body;
|
|
27
|
+
const frontMatterLineCount = frontmatter.raw.split(/\r?\n/).length;
|
|
27
28
|
findings.push(...validateDescription(skill, description));
|
|
28
|
-
findings.push(...validateBody(skill, body));
|
|
29
|
-
findings.push(...validateProgressiveDisclosure(skill));
|
|
30
|
-
findings.push(...(await validateResources(skill, body)));
|
|
29
|
+
findings.push(...validateBody(skill, body, frontMatterLineCount));
|
|
30
|
+
findings.push(...validateProgressiveDisclosure(skill, body, frontMatterLineCount));
|
|
31
|
+
findings.push(...(await validateResources(skill, body, frontMatterLineCount)));
|
|
31
32
|
findings.push(...(await validateEvals(skill, body)));
|
|
32
33
|
return findings;
|
|
33
34
|
};
|
|
34
35
|
const validateDescription = (skill, description) => {
|
|
35
36
|
const findings = [];
|
|
36
37
|
const normalized = description.trim();
|
|
38
|
+
const line = findContentLine(skill.content, /^\s*description\s*:/i);
|
|
37
39
|
if (normalized.length > 0 && !TRIGGER_PATTERN.test(normalized)) {
|
|
38
40
|
findings.push(createFinding(skill, {
|
|
39
41
|
ruleId: "weak-description-trigger",
|
|
@@ -42,6 +44,7 @@ const validateDescription = (skill, description) => {
|
|
|
42
44
|
title: "Description lacks a clear activation trigger",
|
|
43
45
|
message: "The description should explain when an agent should use this skill.",
|
|
44
46
|
suggestion: 'Use imperative phrasing such as "Use this skill when..." and include concrete task contexts.',
|
|
47
|
+
line,
|
|
45
48
|
}));
|
|
46
49
|
}
|
|
47
50
|
if (VAGUE_DESCRIPTION_PATTERN.test(normalized)) {
|
|
@@ -52,6 +55,7 @@ const validateDescription = (skill, description) => {
|
|
|
52
55
|
title: "Description is too vague",
|
|
53
56
|
message: "A short generic description is unlikely to trigger reliably or explain the skill's scope.",
|
|
54
57
|
suggestion: "Describe what the skill does, when to use it, and important adjacent cases.",
|
|
58
|
+
line,
|
|
55
59
|
}));
|
|
56
60
|
}
|
|
57
61
|
if (IMPLEMENTATION_DESCRIPTION_PATTERN.test(normalized) &&
|
|
@@ -63,11 +67,12 @@ const validateDescription = (skill, description) => {
|
|
|
63
67
|
title: "Description focuses on implementation",
|
|
64
68
|
message: "Descriptions should match user intent rather than the skill's internal mechanics.",
|
|
65
69
|
suggestion: "Rewrite the description around the task the user is trying to accomplish.",
|
|
70
|
+
line,
|
|
66
71
|
}));
|
|
67
72
|
}
|
|
68
73
|
return findings;
|
|
69
74
|
};
|
|
70
|
-
const validateBody = (skill, body) => {
|
|
75
|
+
const validateBody = (skill, body, frontMatterLineCount) => {
|
|
71
76
|
const findings = [];
|
|
72
77
|
if (PLACEHOLDER_PATTERN.test(body)) {
|
|
73
78
|
findings.push(createFinding(skill, {
|
|
@@ -77,6 +82,7 @@ const validateBody = (skill, body) => {
|
|
|
77
82
|
title: "Body contains placeholder text",
|
|
78
83
|
message: "A skill body should contain complete reusable instructions, not placeholders.",
|
|
79
84
|
suggestion: "Replace placeholders with concrete workflow steps, gotchas, examples, or validation guidance.",
|
|
85
|
+
line: findBodyLine(frontMatterLineCount, body, PLACEHOLDER_PATTERN),
|
|
80
86
|
}));
|
|
81
87
|
}
|
|
82
88
|
if (GENERIC_BODY_PATTERN.test(body)) {
|
|
@@ -87,6 +93,7 @@ const validateBody = (skill, body) => {
|
|
|
87
93
|
title: "Body appears generic",
|
|
88
94
|
message: "The body uses generic advice that does not add skill-specific expertise.",
|
|
89
95
|
suggestion: "Replace generic phrasing with concrete project or domain procedures the agent would not already know.",
|
|
96
|
+
line: findBodyLine(frontMatterLineCount, body, GENERIC_BODY_PATTERN),
|
|
90
97
|
}));
|
|
91
98
|
}
|
|
92
99
|
if (!WORKFLOW_STEP_PATTERN.test(body)) {
|
|
@@ -97,6 +104,7 @@ const validateBody = (skill, body) => {
|
|
|
97
104
|
title: "Body lacks concrete workflow structure",
|
|
98
105
|
message: "The body does not appear to include headings, ordered steps, or checklist items.",
|
|
99
106
|
suggestion: "Add a concise workflow, gotchas section, examples, or validation loop.",
|
|
107
|
+
line: findFirstBodyLine(body, frontMatterLineCount),
|
|
100
108
|
}));
|
|
101
109
|
}
|
|
102
110
|
if (TOOL_MENU_PATTERN.test(body)) {
|
|
@@ -107,6 +115,7 @@ const validateBody = (skill, body) => {
|
|
|
107
115
|
title: "Body presents a tool menu without a default",
|
|
108
116
|
message: "Skills should provide defaults rather than long menus of equal options.",
|
|
109
117
|
suggestion: "Pick a default tool and explain when to use a fallback.",
|
|
118
|
+
line: findBodyLine(frontMatterLineCount, body, TOOL_MENU_PATTERN),
|
|
110
119
|
}));
|
|
111
120
|
}
|
|
112
121
|
if (DESTRUCTIVE_PATTERN.test(body) && !SAFETY_PATTERN.test(body)) {
|
|
@@ -117,11 +126,12 @@ const validateBody = (skill, body) => {
|
|
|
117
126
|
title: "Destructive operation lacks safety guidance",
|
|
118
127
|
message: "Destructive, release, migration, or deploy guidance should include validation, preview, backup, or confirmation steps.",
|
|
119
128
|
suggestion: "Add a dry-run, validation, backup, or explicit confirmation requirement before the destructive action.",
|
|
129
|
+
line: findBodyLine(frontMatterLineCount, body, DESTRUCTIVE_PATTERN),
|
|
120
130
|
}));
|
|
121
131
|
}
|
|
122
132
|
return findings;
|
|
123
133
|
};
|
|
124
|
-
const validateProgressiveDisclosure = (skill) => {
|
|
134
|
+
const validateProgressiveDisclosure = (skill, body, frontMatterLineCount) => {
|
|
125
135
|
const findings = [];
|
|
126
136
|
const lineCount = skill.content.split(/\r?\n/).length;
|
|
127
137
|
const tokenEstimate = estimateTokens(skill.content);
|
|
@@ -153,11 +163,12 @@ const validateProgressiveDisclosure = (skill) => {
|
|
|
153
163
|
title: "Resource reference lacks a load trigger",
|
|
154
164
|
message: "The skill references a resource directory generically instead of naming the file and when to load it.",
|
|
155
165
|
suggestion: 'Use specific guidance such as "Read references/api-errors.md if the API returns a non-200 status."',
|
|
166
|
+
line: findBodyLine(frontMatterLineCount, body, GENERIC_REFERENCE_PATTERN),
|
|
156
167
|
}));
|
|
157
168
|
}
|
|
158
169
|
return findings;
|
|
159
170
|
};
|
|
160
|
-
const validateResources = async (skill, body) => {
|
|
171
|
+
const validateResources = async (skill, body, frontMatterLineCount) => {
|
|
161
172
|
const findings = [];
|
|
162
173
|
const referencedPaths = [...new Set(skill.content.match(RESOURCE_REFERENCE_PATTERN) ?? [])];
|
|
163
174
|
for (const referencePath of referencedPaths) {
|
|
@@ -169,6 +180,7 @@ const validateResources = async (skill, body) => {
|
|
|
169
180
|
title: "Resource reference escapes the skill directory",
|
|
170
181
|
message: "The skill references a resource outside the skill directory. Resource references must remain inside scripts/, references/, or assets/ for this skill.",
|
|
171
182
|
suggestion: "Use a path rooted inside the skill (for example references/file.md) without '..' segments.",
|
|
183
|
+
line: findReferenceLine(skill.content, referencePath),
|
|
172
184
|
}));
|
|
173
185
|
continue;
|
|
174
186
|
}
|
|
@@ -181,6 +193,7 @@ const validateResources = async (skill, body) => {
|
|
|
181
193
|
title: "Referenced resource does not exist",
|
|
182
194
|
message: `The skill references ${referencePath}, but that path does not exist inside the skill directory.`,
|
|
183
195
|
suggestion: "Create the referenced file or remove the stale reference.",
|
|
196
|
+
line: findReferenceLine(skill.content, referencePath),
|
|
184
197
|
}));
|
|
185
198
|
continue;
|
|
186
199
|
}
|
|
@@ -192,6 +205,7 @@ const validateResources = async (skill, body) => {
|
|
|
192
205
|
title: "Script reference lacks help guidance",
|
|
193
206
|
message: "Script instructions should document usage or mention --help so agents can learn the interface.",
|
|
194
207
|
suggestion: "Add a short usage example and document that the script supports --help.",
|
|
208
|
+
line: findReferenceLine(skill.content, referencePath),
|
|
195
209
|
}));
|
|
196
210
|
}
|
|
197
211
|
}
|
|
@@ -203,6 +217,7 @@ const validateResources = async (skill, body) => {
|
|
|
203
217
|
title: "Script guidance appears interactive",
|
|
204
218
|
message: "Agents need non-interactive scripts that accept flags, stdin, files, or environment variables.",
|
|
205
219
|
suggestion: "Replace interactive prompts with command-line flags and clear errors for missing inputs.",
|
|
220
|
+
line: findBodyLine(frontMatterLineCount, body, INTERACTIVE_SCRIPT_PATTERN),
|
|
206
221
|
}));
|
|
207
222
|
}
|
|
208
223
|
if (UNPINNED_RUNNER_PATTERN.test(body)) {
|
|
@@ -213,6 +228,7 @@ const validateResources = async (skill, body) => {
|
|
|
213
228
|
title: "Package-runner command is not version-pinned",
|
|
214
229
|
message: "One-off package-runner commands should pin versions when reproducibility matters.",
|
|
215
230
|
suggestion: "Use a versioned command such as npx eslint@9 or uvx ruff@0.8.0.",
|
|
231
|
+
line: findBodyLine(frontMatterLineCount, body, UNPINNED_RUNNER_PATTERN),
|
|
216
232
|
}));
|
|
217
233
|
}
|
|
218
234
|
return findings;
|
|
@@ -285,8 +301,35 @@ const createFinding = (skill, input) => ({
|
|
|
285
301
|
skillName: skill.parseResult.ok
|
|
286
302
|
? readString(skill.parseResult.frontmatter.data.name)
|
|
287
303
|
: skill.directoryName,
|
|
304
|
+
line: input.line,
|
|
288
305
|
agentRepairable: true,
|
|
289
306
|
});
|
|
307
|
+
const findContentLine = (content, pattern) => {
|
|
308
|
+
const linePattern = typeof pattern === "string"
|
|
309
|
+
? new RegExp(escapeRegExp(pattern))
|
|
310
|
+
: new RegExp(pattern.source, pattern.flags.replace(/g/g, ""));
|
|
311
|
+
const lines = content.split(/\r?\n/);
|
|
312
|
+
for (const [index, line] of lines.entries()) {
|
|
313
|
+
if (linePattern.test(line)) {
|
|
314
|
+
return index + 1;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
};
|
|
319
|
+
const findBodyLine = (frontMatterLineCount, body, pattern) => {
|
|
320
|
+
const bodyLine = findContentLine(body, pattern);
|
|
321
|
+
if (bodyLine === undefined)
|
|
322
|
+
return undefined;
|
|
323
|
+
return frontMatterLineCount + 2 + bodyLine;
|
|
324
|
+
};
|
|
325
|
+
const findReferenceLine = (content, referencePath) => findContentLine(content, referencePath);
|
|
326
|
+
const findFirstBodyLine = (body, frontMatterLineCount) => {
|
|
327
|
+
const lines = body.split(/\r?\n/);
|
|
328
|
+
if (lines.length === 0)
|
|
329
|
+
return undefined;
|
|
330
|
+
return frontMatterLineCount + 2 + 1;
|
|
331
|
+
};
|
|
332
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
290
333
|
const readString = (value) => typeof value === "string" ? value : undefined;
|
|
291
334
|
const estimateTokens = (content) => Math.ceil(content.split(/\s+/).filter(Boolean).length * 1.35);
|
|
292
335
|
const exists = async (targetPath) => {
|
|
@@ -303,7 +346,7 @@ const resourceCategory = (referencePath) => {
|
|
|
303
346
|
return "references";
|
|
304
347
|
if (referencePath.startsWith("scripts/"))
|
|
305
348
|
return "scripts";
|
|
306
|
-
return "
|
|
349
|
+
return "assets";
|
|
307
350
|
};
|
|
308
351
|
const isNonTrivialSkill = (body) => body.length > 500 || WORKFLOW_STEP_PATTERN.test(body) || RESOURCE_REFERENCE_PATTERN.test(body);
|
|
309
352
|
const hasParentTraversal = (referencePath) => referencePath.split(/[\\/]+/).includes("..");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan-skills.d.ts","sourceRoot":"","sources":["../../src/domain/scan-skills.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAuB,UAAU,EAAe,SAAS,EAAE,MAAM,YAAY,CAAC;AAE1F,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CACtC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,OAAO,mBAAmB,KAAG,OAAO,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"scan-skills.d.ts","sourceRoot":"","sources":["../../src/domain/scan-skills.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAuB,UAAU,EAAe,SAAS,EAAE,MAAM,YAAY,CAAC;AAE1F,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CACtC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,OAAO,mBAAmB,KAAG,OAAO,CAAC,UAAU,CAkDnF,CAAC"}
|
|
@@ -2,7 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseSkillContent } from "./parse-skill.js";
|
|
4
4
|
import { validateQualityRules } from "./rules/quality.js";
|
|
5
|
-
import {
|
|
5
|
+
import { validateStructuralRules } from "./rules/structural.js";
|
|
6
6
|
export const scanSkillRoots = async (input) => {
|
|
7
7
|
const skills = [];
|
|
8
8
|
const diagnostics = [];
|
|
@@ -24,7 +24,6 @@ export const scanSkillRoots = async (input) => {
|
|
|
24
24
|
const skillPath = path.join(skillDir, "SKILL.md");
|
|
25
25
|
const content = await readFile(skillPath, "utf8").catch(() => null);
|
|
26
26
|
if (content === null) {
|
|
27
|
-
findings.push(buildMissingSkillFinding({ root, skillDir }));
|
|
28
27
|
continue;
|
|
29
28
|
}
|
|
30
29
|
skills.push({
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type ParseFailure = {
|
|
|
21
21
|
readonly message: string;
|
|
22
22
|
};
|
|
23
23
|
export type FindingSeverity = "error" | "warning" | "advice";
|
|
24
|
-
export type FindingCategory = "frontmatter" | "description" | "body-quality" | "progressive-disclosure" | "references" | "scripts" | "evals" | "portability" | "cross-ecosystem";
|
|
24
|
+
export type FindingCategory = "frontmatter" | "description" | "body-quality" | "progressive-disclosure" | "references" | "assets" | "scripts" | "evals" | "portability" | "cross-ecosystem";
|
|
25
25
|
export type Finding = {
|
|
26
26
|
readonly ruleId: string;
|
|
27
27
|
readonly severity: FindingSeverity;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/domain/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/domain/types.ts"],"names":[],"mappings":"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"}
|