@skill-harness/cli 0.5.0 → 0.6.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/assets/report.template.html +12 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +91 -13
- package/package.json +3 -3
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
.cell .lm { display: block; font-size: 10px; font-weight: 700; }
|
|
44
44
|
.cell .lm.up { color: var(--pass); }
|
|
45
45
|
.cell .lm.down { color: var(--fail); }
|
|
46
|
+
/* Amber, like the suspect badge: both mean "this verdict is worth less than it looks". */
|
|
47
|
+
.cell .lm.flip { color: #b45309; }
|
|
46
48
|
aside { width: 0; transition: width .15s ease; overflow: hidden; border-left: 1px solid var(--line); background: var(--panel); }
|
|
47
49
|
aside.open { width: 460px; }
|
|
48
50
|
.panel { width: 460px; padding: 16px 18px; }
|
|
@@ -220,7 +222,15 @@ function render() {
|
|
|
220
222
|
: lc === "regressed"
|
|
221
223
|
? `<span class='lm down' title='red baseline PASS → ${effective(cell)} with the skill'>↓ skill</span>`
|
|
222
224
|
: "";
|
|
223
|
-
|
|
225
|
+
// Run-over-run marker. Only on cells that actually flipped, and deliberately
|
|
226
|
+
// NOT folded into the flakiness number beside it: `flaky 0.00` is a within-run
|
|
227
|
+
// measure, and the case this exists for is a cell that was unanimous in every
|
|
228
|
+
// run and still landed on a different side each time.
|
|
229
|
+
const st = cell.stability;
|
|
230
|
+
const stabMark = st
|
|
231
|
+
? `<span class='lm flip' title='${escapeHtml(st.note)}'>⇄ ${st.flips}/${st.compared}</span>`
|
|
232
|
+
: "";
|
|
233
|
+
html += `<td class='cell ${v}${sel}' data-col='${col.index}' data-id='${scn.id}'>${v}${ov}${suspectBadge}${liftMark}${stabMark}${reps}</td>`;
|
|
224
234
|
});
|
|
225
235
|
html += "</tr>";
|
|
226
236
|
}
|
|
@@ -247,6 +257,7 @@ async function openPanel(colIndex, scenarioId) {
|
|
|
247
257
|
<div class="meta">${escapeHtml(col.label)} · mode ${escapeHtml(col.mode || "green")} · judge ${escapeHtml(col.judge.provider + ":" + col.judge.model)}</div>
|
|
248
258
|
<div class="reason"><b>judge:</b> ${cell.judge_verdict} — ${escapeHtml(cell.judge_reason || "(no reason)")}</div>
|
|
249
259
|
${cell.suspect ? `<div class="reason" style="color:#b45309"><b>⚠ suspect:</b> judge listed no failed item — re-judge before trusting this FAIL</div>` : ""}
|
|
260
|
+
${cell.stability ? `<div class="reason" style="color:#b45309"><b>⇄ boundary cell:</b> ${escapeHtml(cell.stability.note)}</div>` : ""}
|
|
250
261
|
<div class="toggle">
|
|
251
262
|
<button data-v="PASS" class="PASS ${cell.override === 'PASS' ? 'active PASS' : ''}">PASS</button>
|
|
252
263
|
<button data-v="FAIL" class="FAIL ${cell.override === 'FAIL' ? 'active FAIL' : ''}">FAIL</button>
|
package/dist/cli.d.ts
CHANGED
|
@@ -23,7 +23,15 @@ export declare function cmdGrade(args: Args, adapterOverride?: HarnessAdapter):
|
|
|
23
23
|
export declare function cmdRegate(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
|
|
24
24
|
export declare function cmdInit(args: Args): Promise<void>;
|
|
25
25
|
export declare function cmdSuggest(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Exit-code contract: 0 = no gate-failing findings, 1 = >=1 of them, or a resolution
|
|
28
|
+
* error (unknown skill/root, no skills with a spec).
|
|
29
|
+
*
|
|
30
|
+
* `info` findings (run-over-run stability notes) print and annotate but never fail the
|
|
31
|
+
* gate: a boundary cell says how much one run of a scenario is worth, which is not a
|
|
32
|
+
* defect in the spec, the fixtures or the results. A linter that reddens CI for it would
|
|
33
|
+
* teach everyone to stop reading it.
|
|
34
|
+
*/
|
|
27
35
|
export declare function cmdLint(args: Args): Promise<void>;
|
|
28
36
|
/**
|
|
29
37
|
* The help text, rendered per call rather than frozen at module load.
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, existsSync, appendFileSync, mkdirSync, writeFileSync, mkd
|
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
|
-
import { discover, resolveSkill, loadSpec, parseSpec, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, regateRun, specPathForRunDir, collectLift, HARNESS_VERSION, defaultJudge, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, } from "@skill-harness/core";
|
|
6
|
+
import { discover, resolveSkill, loadSpec, parseSpec, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, failsGate, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, regateRun, specPathForRunDir, collectLift, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, HARNESS_VERSION, defaultJudge, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, } from "@skill-harness/core";
|
|
7
7
|
import { getAdapter } from "@skill-harness/adapters";
|
|
8
8
|
import { serveReview } from "./serve.js";
|
|
9
9
|
const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
|
|
@@ -183,7 +183,11 @@ export async function cmdRun(args) {
|
|
|
183
183
|
// any earlier run — the tag dir (<harness>-<modelslug>) is the join key.
|
|
184
184
|
const tag = basename(dirname(summary.runDir));
|
|
185
185
|
const lift = collectLift(skill.dir).find((l) => l.tag === tag);
|
|
186
|
-
|
|
186
|
+
// Stability is derived from history INCLUDING the run just written, and scoped to
|
|
187
|
+
// this tag + mode: another model's flips under this model's scorecard would be a
|
|
188
|
+
// worse error than not reporting them at all.
|
|
189
|
+
const stability = collectStability(skill.dir).filter((c) => c.tag === tag && c.mode === summary.results.mode);
|
|
190
|
+
console.log("\n" + formatScorecard(summary, lift, stability) + "\n");
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
193
|
console.log(`\nReview interactively: skill-harness review ${skills[0]?.name ?? "<skill>"} --skills ${root}`);
|
|
@@ -302,6 +306,68 @@ export async function cmdRegate(args, adapterOverride) {
|
|
|
302
306
|
// flipped reps, which it must not spend silently.
|
|
303
307
|
console.log(`\n${runDirs.length} run(s) re-gated, ${moved} verdict(s) moved, ${calls} judge call(s) (no model re-runs).`);
|
|
304
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Run-over-run verdict stability, derived from committed results. Free and offline: it
|
|
311
|
+
* reads results.yaml files and computes — no model, no judge, no harness.
|
|
312
|
+
*
|
|
313
|
+
* Exits 0 whatever it finds. A boundary cell is not a defect in the skill or in the
|
|
314
|
+
* spec; it is a statement about how much one run of that cell is worth. Making it a
|
|
315
|
+
* gate would turn "this needs more reps" into "your build is broken".
|
|
316
|
+
*/
|
|
317
|
+
async function cmdStability(args) {
|
|
318
|
+
const root = flagStr(args, "skills", process.cwd());
|
|
319
|
+
const target = args._[0] ?? "all";
|
|
320
|
+
const windowRaw = flagStr(args, "window");
|
|
321
|
+
const window = windowRaw ? Number(windowRaw) : undefined;
|
|
322
|
+
if (windowRaw !== undefined && (!Number.isInteger(window) || window < 2)) {
|
|
323
|
+
throw new Error(`--window must be an integer >= 2 (got \`${windowRaw}\`) — one run has no run-over-run step`);
|
|
324
|
+
}
|
|
325
|
+
const showAll = flagBool(args, "all");
|
|
326
|
+
const skills = target === "all" ? discover(root).filter((s) => s.hasSpec) : [resolveSkill(root, target)];
|
|
327
|
+
if (skills.length === 0)
|
|
328
|
+
throw new Error(`no skills with a spec under ${root}`);
|
|
329
|
+
let boundaries = 0;
|
|
330
|
+
for (const skill of skills) {
|
|
331
|
+
const all = collectStability(skill.dir, { window });
|
|
332
|
+
if (all.length === 0) {
|
|
333
|
+
console.log(`\n${skill.name}: no scored runs yet — stability needs at least two runs of the same skill × model × mode`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// One block per model tag × delivery mode: green and force are different
|
|
337
|
+
// deliveries of the same text, so their histories are never one series.
|
|
338
|
+
const groups = new Map();
|
|
339
|
+
for (const s of all) {
|
|
340
|
+
const key = `${s.tag} · mode=${s.mode}`;
|
|
341
|
+
(groups.get(key) ?? groups.set(key, []).get(key)).push(s);
|
|
342
|
+
}
|
|
343
|
+
console.log(`\n── ${skill.name} ──`);
|
|
344
|
+
for (const [key, cells] of groups) {
|
|
345
|
+
const runs = Math.max(...cells.map((c) => c.points.length));
|
|
346
|
+
console.log(` ${key} (${runs} run(s) in the window)`);
|
|
347
|
+
const boundary = boundaryCells(cells);
|
|
348
|
+
boundaries += boundary.length;
|
|
349
|
+
for (const s of boundary) {
|
|
350
|
+
console.log(` ⇄ ${s.critical ? "CRITICAL " : ""}${stabilityNote(s)}`);
|
|
351
|
+
}
|
|
352
|
+
if (showAll) {
|
|
353
|
+
for (const s of cells) {
|
|
354
|
+
if (s.state !== "boundary")
|
|
355
|
+
console.log(` ${s.state === "stable" ? "=" : "?"} ${stabilityNote(s)}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const stable = cells.filter((c) => c.state === "stable").length;
|
|
360
|
+
const unmeasured = cells.filter((c) => c.state === "unmeasured").length;
|
|
361
|
+
console.log(` ${stable} held their verdict · ${unmeasured} with no comparable step (--all to list them)`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
console.log(`\n${boundaries} boundary cell(s). ${PATH_LEGEND}`);
|
|
366
|
+
if (boundaries > 0) {
|
|
367
|
+
console.log(`A boundary cell is worth re-running with more reps (--reps) before you trust one run of it;`);
|
|
368
|
+
console.log(`within-run flakiness cannot see this, because it only ever looks at one run.`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
305
371
|
async function cmdReview(args) {
|
|
306
372
|
const root = flagStr(args, "skills", process.cwd());
|
|
307
373
|
const target = args._[0];
|
|
@@ -435,7 +501,15 @@ export async function cmdSuggest(args, adapterOverride) {
|
|
|
435
501
|
rmSync(cwd, { recursive: true, force: true });
|
|
436
502
|
}
|
|
437
503
|
}
|
|
438
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* Exit-code contract: 0 = no gate-failing findings, 1 = >=1 of them, or a resolution
|
|
506
|
+
* error (unknown skill/root, no skills with a spec).
|
|
507
|
+
*
|
|
508
|
+
* `info` findings (run-over-run stability notes) print and annotate but never fail the
|
|
509
|
+
* gate: a boundary cell says how much one run of a scenario is worth, which is not a
|
|
510
|
+
* defect in the spec, the fixtures or the results. A linter that reddens CI for it would
|
|
511
|
+
* teach everyone to stop reading it.
|
|
512
|
+
*/
|
|
439
513
|
export async function cmdLint(args) {
|
|
440
514
|
const root = flagStr(args, "skills", process.cwd());
|
|
441
515
|
const target = args._[0] ?? "all";
|
|
@@ -466,18 +540,20 @@ export async function cmdLint(args) {
|
|
|
466
540
|
f = [{ skill: dir, code: "lint-error", message: e instanceof Error ? e.message : String(e) }];
|
|
467
541
|
}
|
|
468
542
|
findings.push(...f);
|
|
469
|
-
if (f.length === 0)
|
|
543
|
+
if (f.filter(failsGate).length === 0)
|
|
470
544
|
console.log(`✓ ${dir}`);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
545
|
+
for (const x of f) {
|
|
546
|
+
const where = x.scenario ? `${dir}/${x.scenario}` : dir; // dir-based label, consistent with the ✓ line
|
|
547
|
+
const fails = failsGate(x);
|
|
548
|
+
console.log(`${fails ? "✗" : "ℹ"} ${where}: ${x.code} — ${x.message}`);
|
|
549
|
+
if (gha)
|
|
550
|
+
console.log(`::${fails ? "error" : "notice"} title=skill-harness::${where}: ${x.code} — ${x.message}`);
|
|
551
|
+
}
|
|
478
552
|
}
|
|
479
|
-
|
|
480
|
-
|
|
553
|
+
const gating = findings.filter(failsGate).length;
|
|
554
|
+
const notes = findings.length - gating;
|
|
555
|
+
console.log(`\n${skillDirs.length} skill(s), ${gating} finding(s)${notes > 0 ? `, ${notes} note(s) (do not fail the gate)` : ""}`);
|
|
556
|
+
process.exitCode = gating > 0 ? 1 : 0;
|
|
481
557
|
}
|
|
482
558
|
// ---------------------------------------------------------------- dispatch
|
|
483
559
|
/**
|
|
@@ -497,6 +573,7 @@ export function help() {
|
|
|
497
573
|
grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
|
|
498
574
|
rescore <run-dir>... re-score saved reps vs current spec thresholds (free)
|
|
499
575
|
regate <run-dir>... [--judge prov:model] re-evaluate diff needles against the saved diffs (free; judges only reps whose gate flipped)
|
|
576
|
+
stability <skill|all> --skills <root> [--window N] [--all] run-over-run verdict flips per scenario (free, offline)
|
|
500
577
|
review <skill> --skills <root> [--port N] serve the interactive review UI
|
|
501
578
|
add-test <skill> --skills <root> --id ID --title T --turn ... --check ... [--critical] [--mode seeded --fixture path]
|
|
502
579
|
init <skill> --skills <root> [--force] scaffold a commented template spec (free, offline)
|
|
@@ -521,6 +598,7 @@ export async function main(argv) {
|
|
|
521
598
|
case "grade": return cmdGrade(args);
|
|
522
599
|
case "rescore": return cmdRescore(args);
|
|
523
600
|
case "regate": return cmdRegate(args);
|
|
601
|
+
case "stability": return cmdStability(args);
|
|
524
602
|
case "review": return cmdReview(args);
|
|
525
603
|
case "add-test": return cmdAddTest(args);
|
|
526
604
|
case "init": return cmdInit(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "skill-harness CLI — run, grade, review, and lint agent-skill scenarios on the pi harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepack": "rm -rf ./assets && mkdir -p ./assets && cp ../../assets/report.* ./assets/ && cp ../../LICENSE ./LICENSE"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@skill-harness/core": "0.
|
|
48
|
-
"@skill-harness/adapters": "0.
|
|
47
|
+
"@skill-harness/core": "0.6.0",
|
|
48
|
+
"@skill-harness/adapters": "0.6.0"
|
|
49
49
|
}
|
|
50
50
|
}
|