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 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
@@ -10,4 +10,5 @@ if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
10
10
  }
11
11
  }
12
12
 
13
- await import("../dist/cli/index.js");
13
+ const { runCli } = await import("../dist/cli/index.js");
14
+ await runCli();
@@ -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
@@ -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
@@ -30,4 +30,6 @@ export const main = async (argv = process.argv) => {
30
30
  process.stdin.unref?.();
31
31
  }
32
32
  };
33
- main().catch(handleCliError);
33
+ export const runCli = async (argv = process.argv) => {
34
+ await main(argv).catch(handleCliError);
35
+ };
@@ -34,12 +34,20 @@ export const prepareRepairHandoff = async (input) => {
34
34
  };
35
35
  };
36
36
  const chooseRepairFindings = async (report, prompts) => {
37
- const subset = await prompts.select("Choose findings to repair", [
38
- { name: "Blocking errors only", value: "errors" },
39
- { name: "Blocking errors and warnings", value: "errors-and-warnings" },
40
- { name: "All findings", value: "all" },
41
- { name: "Selected skills", value: "selected-skills" },
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.trim();
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-doctor",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Interactive CLI that scans and repairs Agent Skills quality issues.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.13",