skills-doctor 0.3.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 +11 -0
- package/bin/skills-doctor.js +2 -1
- 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 +50 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,17 @@ 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
|
+
|
|
11
22
|
## [0.3.0] - 2026-06-16
|
|
12
23
|
|
|
13
24
|
### Added
|
package/bin/skills-doctor.js
CHANGED
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) => {
|