@yogiadhik/skillcheck 0.1.2 → 0.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
@@ -53,8 +53,9 @@ case, no re-run — and `--budget 2.50` stops the run once the cap is
53
53
  crossed. The check happens between cases, so one long case can still
54
54
  overshoot on its own: pair a budget with `--model haiku` to keep
55
55
  single cases cheap. skillcheck tells you when no model is set, and
56
- warns at the end if a run blew past its budget. Lint never spends
57
- anything.
56
+ warns at the end if a run blew past its budget. `run --dry-run` shows
57
+ which cases would execute and which would replay from cache before
58
+ anything spends a cent. Lint never spends anything.
58
59
 
59
60
  ## CI
60
61
 
@@ -66,6 +67,42 @@ That runs lint with GitHub annotations on every pull request. For
66
67
  behavioral runs in CI, call `npx @yogiadhik/skillcheck run` yourself in a job that
67
68
  has your agent authenticated; keep a budget flag on it.
68
69
 
70
+ ## what it finds in the wild
71
+
72
+ I periodically run lint across public skill collections — official
73
+ vendor skills, a security firm's suite, several large community
74
+ bundles, close to eight thousand skill files so far. No names,
75
+ because the point is the pattern, and the pattern repeats:
76
+
77
+ - **Skills that silently don't work.** If a skill's frontmatter isn't
78
+ valid YAML, the skill never loads — and nothing tells you. The usual
79
+ cause is an unquoted description containing a colon. Every large
80
+ bundle I've audited shipped a few of these; anyone installing them
81
+ gets a skill that simply never fires.
82
+ - **Instructions pointing at files that don't exist.** Skills tell the
83
+ agent "read references/details.md for the full procedure" and that
84
+ file was never committed. The agent hits a dead end in the middle of
85
+ someone's task. This is the most common hard error.
86
+ - **Sheer size.** The most common warning by a wide margin. Skills
87
+ running twenty to forty thousand tokens re-bill that entire document
88
+ on every single invocation.
89
+ - **Descriptions without trigger language.** The description is the
90
+ only thing the model reads when deciding whether to use a skill; a
91
+ description that doesn't say when to use it is a skill that fires
92
+ rarely and randomly.
93
+ - **Well-maintained collections come back clean.** Several do,
94
+ official and community both. Quality is achievable; it's just not
95
+ the default.
96
+
97
+ Two honest footnotes. First: twice, the loudest "errors" in an audit
98
+ turned out to be this linter's own false positives — links inside
99
+ example code blocks, and templated {baseDir} targets that resolve at
100
+ runtime. Both fixed (0.1.1, 0.1.3), both caught by checking findings
101
+ against the accused code before believing them. Do the same with
102
+ anything this tool tells you. Second: these audits are how skillcheck
103
+ gets tested — every rule here has drawn blood on real files or been
104
+ fixed for crying wolf.
105
+
69
106
  ## a note on safety
70
107
 
71
108
  `run` executes the agent with permission prompts disabled, inside a
package/dist/cli.js CHANGED
@@ -3,13 +3,13 @@ import { Command } from 'commander';
3
3
  import { initTestFile } from './init.js';
4
4
  import { lintPath } from './lint/lint.js';
5
5
  import { printGithubAnnotations } from './reporters/github.js';
6
- import { printFindings, printReports } from './reporters/terminal.js';
7
- import { discoverTestFiles, reportsPass, runTests } from './runner.js';
6
+ import { printFindings, printPlan, printReports } from './reporters/terminal.js';
7
+ import { discoverTestFiles, planTests, reportsPass, runTests } from './runner.js';
8
8
  const program = new Command();
9
9
  program
10
10
  .name('skillcheck')
11
11
  .description('lint and behavioral tests for agent skills')
12
- .version('0.1.2');
12
+ .version('0.2.0');
13
13
  program
14
14
  .command('lint')
15
15
  .argument('[path]', 'skill file or directory', '.')
@@ -39,9 +39,18 @@ program
39
39
  .option('--budget <usd>', 'stop once this much has been spent')
40
40
  .option('--timeout <seconds>', 'per-case timeout', '300')
41
41
  .option('--no-cache', 'ignore and do not write cached results')
42
+ .option('--dry-run', 'show what would run and what would replay from cache, spending nothing')
42
43
  .option('--json', 'machine-readable output')
43
44
  .action(async (path, opts) => {
44
45
  try {
46
+ if (opts.dryRun) {
47
+ const plans = planTests(path, { model: opts.model });
48
+ if (opts.json)
49
+ console.log(JSON.stringify(plans, null, 2));
50
+ else
51
+ printPlan(plans);
52
+ return;
53
+ }
45
54
  const files = discoverTestFiles(path);
46
55
  if (!opts.json) {
47
56
  console.log(`${files.length} test file(s); live agent runs cost money — cap with --budget`);
@@ -50,6 +50,9 @@ export const relativeLinks = (s) => {
50
50
  const target = m[1];
51
51
  if (/^(https?:|mailto:|#)/.test(target))
52
52
  continue;
53
+ // templated targets like {baseDir}/x.md resolve at runtime; can't judge them
54
+ if (target.includes('{'))
55
+ continue;
53
56
  if (!existsSync(resolve(s.dir, target.split('#')[0])))
54
57
  out.push(finding(s, 'relative-links', 'error', `broken relative link: ${target}`));
55
58
  }
@@ -7,6 +7,25 @@ export function printFindings(findings) {
7
7
  const errors = findings.filter((f) => f.severity === 'error').length;
8
8
  console.log(`\n${findings.length} finding(s), ${errors} error(s)`);
9
9
  }
10
+ export function printPlan(plans) {
11
+ let live = 0;
12
+ let knownCost = 0;
13
+ for (const p of plans) {
14
+ if (p.cached) {
15
+ knownCost += p.lastCostUsd ?? 0;
16
+ console.log(`${pc.dim('cached ')} ${p.case} ($${(p.lastCostUsd ?? 0).toFixed(4)} last time, replays free)`);
17
+ }
18
+ else {
19
+ live++;
20
+ console.log(`${pc.yellow('would run')} ${p.case}`);
21
+ }
22
+ }
23
+ console.log(`\n${plans.length} case(s): ${live} would run live, ${plans.length - live} would replay from cache`);
24
+ if (live > 0)
25
+ console.log(pc.yellow(`live cases cost real money and their price is unknown until they run — pair --model haiku with --budget`));
26
+ else if (plans.length > 0)
27
+ console.log(pc.dim(`nothing to spend — a real run would replay entirely from cache ($${knownCost.toFixed(4)} when first run)`));
28
+ }
10
29
  export function printReports(reports, budgetUsd) {
11
30
  let cost = 0;
12
31
  for (const r of reports) {
package/dist/runner.js CHANGED
@@ -29,6 +29,22 @@ export function loadTestFile(path) {
29
29
  throw new Error(`${path}: needs a "skill" path and a "cases" list`);
30
30
  return { path, skill: doc.skill, cases: doc.cases };
31
31
  }
32
+ function caseKey(skillDir, fileDir, c, model) {
33
+ return hashInputs(c.setup ? [skillDir, resolve(fileDir, c.setup)] : skillDir, YAML.stringify(c) + '\0' + (model ?? ''));
34
+ }
35
+ export function planTests(root, opts = {}) {
36
+ const cache = new ResultCache(join(process.cwd(), '.skillcheck', 'cache.json'));
37
+ const plans = [];
38
+ for (const file of discoverTestFiles(root)) {
39
+ const testFile = loadTestFile(file);
40
+ const skillDir = resolve(dirname(file), testFile.skill);
41
+ for (const c of testFile.cases) {
42
+ const hit = cache.get(caseKey(skillDir, dirname(file), c, opts.model));
43
+ plans.push({ file, case: c.name, cached: !!hit, lastCostUsd: hit?.costUsd });
44
+ }
45
+ }
46
+ return plans;
47
+ }
32
48
  export async function runTests(root, opts = {}) {
33
49
  const adapter = opts.adapter ?? claudeCode;
34
50
  const cache = new ResultCache(join(process.cwd(), '.skillcheck', 'cache.json'));
@@ -49,7 +65,7 @@ export async function runTests(root, opts = {}) {
49
65
  });
50
66
  continue;
51
67
  }
52
- const key = hashInputs(c.setup ? [skillDir, resolve(dirname(file), c.setup)] : skillDir, YAML.stringify(c) + '\0' + (opts.model ?? ''));
68
+ const key = caseKey(skillDir, dirname(file), c, opts.model);
53
69
  if (opts.cache !== false) {
54
70
  const hit = cache.get(key);
55
71
  if (hit) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yogiadhik/skillcheck",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "lint and behavioral tests for agent skills",
5
5
  "license": "MIT",
6
6
  "author": "Yogesh Adhikari",