@yogiadhik/skillcheck 0.1.3 → 0.2.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/README.md +9 -7
- package/dist/cli.js +12 -3
- package/dist/lint/rules.js +7 -2
- package/dist/reporters/terminal.js +19 -0
- package/dist/runner.js +17 -1
- package/package.json +2 -2
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.
|
|
57
|
-
|
|
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
|
|
|
@@ -93,11 +94,12 @@ because the point is the pattern, and the pattern repeats:
|
|
|
93
94
|
official and community both. Quality is achievable; it's just not
|
|
94
95
|
the default.
|
|
95
96
|
|
|
96
|
-
Two honest footnotes. First:
|
|
97
|
-
turned out to be this linter's own false positives — links
|
|
98
|
-
example code blocks,
|
|
99
|
-
runtime
|
|
100
|
-
|
|
97
|
+
Two honest footnotes. First: three times now, the loudest "errors" in
|
|
98
|
+
an audit turned out to be this linter's own false positives — links
|
|
99
|
+
inside example code blocks, templated {baseDir} targets that resolve
|
|
100
|
+
at runtime, and placeholder links in quoted prose like
|
|
101
|
+
"[Team roster](its link)". All fixed (0.1.1, 0.1.3, 0.2.1), all caught
|
|
102
|
+
by checking findings against the accused code before believing them. Do the same with
|
|
101
103
|
anything this tool tells you. Second: these audits are how skillcheck
|
|
102
104
|
gets tested — every rule here has drawn blood on real files or been
|
|
103
105
|
fixed for crying wolf.
|
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.
|
|
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`);
|
package/dist/lint/rules.js
CHANGED
|
@@ -18,8 +18,10 @@ export const descriptionQuality = (s) => {
|
|
|
18
18
|
const out = [];
|
|
19
19
|
if (d.length < 20)
|
|
20
20
|
out.push(finding(s, 'description-quality', 'warning', 'description under 20 chars; unlikely to trigger reliably'));
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// claude code truncates description + when_to_use at 1536 chars combined
|
|
22
|
+
const whenToUse = typeof s.frontmatter.when_to_use === 'string' ? s.frontmatter.when_to_use : '';
|
|
23
|
+
if (d.length + whenToUse.length > 1536)
|
|
24
|
+
out.push(finding(s, 'description-quality', 'warning', 'description over 1536 chars (counting when_to_use); gets truncated in listings'));
|
|
23
25
|
if (!/\b(use|when|for|trigger)\b/i.test(d))
|
|
24
26
|
out.push(finding(s, 'description-quality', 'warning', 'description has no trigger language ("use when ...") — the agent decides from this text alone'));
|
|
25
27
|
return out;
|
|
@@ -53,6 +55,9 @@ export const relativeLinks = (s) => {
|
|
|
53
55
|
// templated targets like {baseDir}/x.md resolve at runtime; can't judge them
|
|
54
56
|
if (target.includes('{'))
|
|
55
57
|
continue;
|
|
58
|
+
// a destination with a raw space never parses as a markdown link; it's placeholder prose
|
|
59
|
+
if (/\s/.test(target))
|
|
60
|
+
continue;
|
|
56
61
|
if (!existsSync(resolve(s.dir, target.split('#')[0])))
|
|
57
62
|
out.push(finding(s, 'relative-links', 'error', `broken relative link: ${target}`));
|
|
58
63
|
}
|
|
@@ -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 =
|
|
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
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "lint and behavioral tests for agent skills",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Yogesh Adhikari",
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
"@types/cross-spawn": "^6.0.6",
|
|
30
30
|
"@types/node": "^26.1.1",
|
|
31
31
|
"typescript": "^7.0.2",
|
|
32
|
-
"vitest": "^
|
|
32
|
+
"vitest": "^5.0.2"
|
|
33
33
|
}
|
|
34
34
|
}
|