@skill-harness/cli 0.4.0 → 0.5.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.grade.js +3 -1
- package/assets/report.template.html +13 -8
- package/dist/cli.js +6 -0
- package/dist/serve.js +21 -9
- package/package.json +3 -3
package/assets/report.grade.js
CHANGED
|
@@ -143,7 +143,9 @@ export function liftNoneBadge(col) {
|
|
|
143
143
|
if (col.lift) {
|
|
144
144
|
return { text: "lift not comparable", title: col.liftHeadline || "nothing in the red baseline could be compared" };
|
|
145
145
|
}
|
|
146
|
-
|
|
146
|
+
// Green and force are both skill-delivered, so both can be missing a baseline.
|
|
147
|
+
// A red column is the baseline and gets no badge at all.
|
|
148
|
+
if (col.mode === "green" || col.mode === "force") {
|
|
147
149
|
return { text: "no red baseline", title: "run the same scenarios with --mode red to get a baseline" };
|
|
148
150
|
}
|
|
149
151
|
return null;
|
|
@@ -126,10 +126,10 @@ function render() {
|
|
|
126
126
|
const grades = DATA.columns.map((col) => gradeColumn(col, DATA.shipBar, DATA.critical));
|
|
127
127
|
const queue = [];
|
|
128
128
|
DATA.columns.forEach((col) => {
|
|
129
|
-
// A suspect cell in a red
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
if (col.mode && col.mode !== "green") return;
|
|
129
|
+
// A suspect cell in a red column can't be re-judged from here (/rejudge 400s
|
|
130
|
+
// for an unscored baseline) — don't queue it. The matrix below still shows
|
|
131
|
+
// suspect badges for every column regardless of mode.
|
|
132
|
+
if (col.mode && col.mode !== "green" && col.mode !== "force") return;
|
|
133
133
|
for (const scn of DATA.scenarios) {
|
|
134
134
|
const cell = col.cells[scn.id];
|
|
135
135
|
if (cell && cell.suspect && !cell.override) queue.push({ col: col.index, id: scn.id, label: col.label, reason: cell.judge_reason });
|
|
@@ -150,8 +150,8 @@ function render() {
|
|
|
150
150
|
DATA.columns.forEach((col, i) => {
|
|
151
151
|
const g = grades[i];
|
|
152
152
|
let gradeHtml;
|
|
153
|
-
if (col.mode && col.mode !== "green") {
|
|
154
|
-
//
|
|
153
|
+
if (col.mode && col.mode !== "green" && col.mode !== "force") {
|
|
154
|
+
// Green and force are scored; a red baseline column has no ship grade.
|
|
155
155
|
gradeHtml = `<span class='badge no'>not scored (${escapeHtml(col.mode)})</span>`;
|
|
156
156
|
if (g.suspect > 0) gradeHtml += ` — ${g.suspect} suspect`;
|
|
157
157
|
} else {
|
|
@@ -244,7 +244,7 @@ async function openPanel(colIndex, scenarioId) {
|
|
|
244
244
|
panel.innerHTML = `
|
|
245
245
|
<button class="close" id="closeBtn">×</button>
|
|
246
246
|
<h2>${scn.id} · ${escapeHtml(scn.title)}</h2>
|
|
247
|
-
<div class="meta">${escapeHtml(col.label)} · judge ${escapeHtml(col.judge.provider + ":" + col.judge.model)}</div>
|
|
247
|
+
<div class="meta">${escapeHtml(col.label)} · mode ${escapeHtml(col.mode || "green")} · judge ${escapeHtml(col.judge.provider + ":" + col.judge.model)}</div>
|
|
248
248
|
<div class="reason"><b>judge:</b> ${cell.judge_verdict} — ${escapeHtml(cell.judge_reason || "(no reason)")}</div>
|
|
249
249
|
${cell.suspect ? `<div class="reason" style="color:#b45309"><b>⚠ suspect:</b> judge listed no failed item — re-judge before trusting this FAIL</div>` : ""}
|
|
250
250
|
<div class="toggle">
|
|
@@ -358,7 +358,12 @@ function renderTrends(data) {
|
|
|
358
358
|
const badge = last.ship ? "<span class='badge ship'>SHIP</span>" : "<span class='badge no'>NOT READY</span>";
|
|
359
359
|
const trunc = m.truncated ? ` <span class='dim'>(last ${m.runs.length})</span>` : "";
|
|
360
360
|
const skippedNote = m.skipped > 0 ? ` <span class='dim'>(${m.skipped} unreadable)</span>` : "";
|
|
361
|
-
|
|
361
|
+
// The mode is part of the series identity, not decoration: a tag that moved
|
|
362
|
+
// from green to force delivery has TWO series here, and the same skill text
|
|
363
|
+
// scores differently under each — so an unlabelled pair of sparklines would
|
|
364
|
+
// read as one history that jumped.
|
|
365
|
+
const modeNote = m.mode ? ` <span class='dim'>${escapeHtml(m.mode)}</span>` : "";
|
|
366
|
+
html += `<div class="tmodel"><div class="tmodel-h">${escapeHtml(m.model)}${modeNote} — ${sparkline(m.runs)} ${last.letter} (${last.pct}%) ${badge}${trunc}${skippedNote}</div>`;
|
|
362
367
|
html += "<table class='tgrid'><thead><tr><th></th>";
|
|
363
368
|
for (const run of m.runs) html += `<th title="${escapeHtml(run.label || run.timestamp)}">${escapeHtml((run.label || run.timestamp).slice(0, 8))}</th>`;
|
|
364
369
|
html += "</tr></thead><tbody>";
|
package/dist/cli.js
CHANGED
|
@@ -134,6 +134,7 @@ export async function cmdRun(args) {
|
|
|
134
134
|
if (!(await adapter.available()))
|
|
135
135
|
throw new Error(`harness \`${harnessName}\` is not on PATH`);
|
|
136
136
|
const mode = flagStr(args, "mode", "green") || "green";
|
|
137
|
+
const canary = flagBool(args, "canary");
|
|
137
138
|
const label = flagStr(args, "label") || null;
|
|
138
139
|
const parallel = Math.max(1, Number(flagStr(args, "parallel", "1")) || 1);
|
|
139
140
|
const { reps, passThreshold } = parseRunTuning(args);
|
|
@@ -174,6 +175,7 @@ export async function cmdRun(args) {
|
|
|
174
175
|
reps,
|
|
175
176
|
passThreshold,
|
|
176
177
|
only,
|
|
178
|
+
canary,
|
|
177
179
|
onProgress: (m) => console.log(m),
|
|
178
180
|
});
|
|
179
181
|
summaries.push(summary);
|
|
@@ -491,6 +493,7 @@ export function help() {
|
|
|
491
493
|
|
|
492
494
|
run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
|
|
493
495
|
[--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
|
|
496
|
+
[--canary] green only: spend ONE probe proving the skill reached the model, and abort the run if it did not
|
|
494
497
|
grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
|
|
495
498
|
rescore <run-dir>... re-score saved reps vs current spec thresholds (free)
|
|
496
499
|
regate <run-dir>... [--judge prov:model] re-evaluate diff needles against the saved diffs (free; judges only reps whose gate flipped)
|
|
@@ -504,6 +507,9 @@ export function help() {
|
|
|
504
507
|
version print ${HARNESS_VERSION} and exit (also --version / -v)
|
|
505
508
|
|
|
506
509
|
defaults: model=${DEFAULT_MODEL} judge=${defaultJudge()} mode=green harness=pi
|
|
510
|
+
green and force are both scored; red is the unscored baseline. green delivery depends on the
|
|
511
|
+
harness version (pi >= 0.83.0 discloses only the skill's description and loads the body on demand),
|
|
512
|
+
so --mode force is the delivery that cannot silently degrade — and --canary proves green per run.
|
|
507
513
|
the judge default is Opus on your Claude subscription (\`claude-code\` → \`claude -p\`), not a
|
|
508
514
|
metered API key. Set SKILL_HARNESS_JUDGE to change it for a repo or a shell; --judge wins over both.`;
|
|
509
515
|
}
|
package/dist/serve.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { collectReport, renderReport, collectTrends, readResults, writeResults, applyOverride, preserveTranscript, findTranscriptFiles, ensureResultsGitignore, appendJournal, loadSpec, regradeScenario, findJudgeRawFiles, effectiveThreshold, envFlag, } from "@skill-harness/core";
|
|
6
|
+
import { collectReport, renderReport, collectTrends, readResults, writeResults, applyOverride, preserveTranscript, findTranscriptFiles, ensureResultsGitignore, appendJournal, loadSpec, regradeScenario, refreshRubricHashes, findJudgeRawFiles, effectiveThreshold, scoreContextFor, isScoredMode, envFlag, } from "@skill-harness/core";
|
|
7
7
|
import { getAdapter } from "@skill-harness/adapters";
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
/** Locate assets/report.template.html relative to dist/ or src/. */
|
|
@@ -99,9 +99,13 @@ export async function serveReview(opts) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
const results = readResults(column.runDir);
|
|
102
|
-
|
|
102
|
+
// Skill-delivered runs only (green or force). A red baseline's transcripts
|
|
103
|
+
// can be re-judged too — `skill-harness grade <run-dir>` does it — but a
|
|
104
|
+
// baseline has no grade for this endpoint to report back, and the button
|
|
105
|
+
// sits under a scorecard.
|
|
106
|
+
if (!isScoredMode(results.mode)) {
|
|
103
107
|
res.writeHead(400, { "content-type": "application/json" });
|
|
104
|
-
res.end(JSON.stringify({ ok: false, error:
|
|
108
|
+
res.end(JSON.stringify({ ok: false, error: `only scored runs (green/force) can be re-judged here — for a ${results.mode} run use \`skill-harness grade\`` }));
|
|
105
109
|
return;
|
|
106
110
|
}
|
|
107
111
|
const specPath = join(opts.skillDir, "tests", "specification.yaml");
|
|
@@ -126,13 +130,21 @@ export async function serveReview(opts) {
|
|
|
126
130
|
try {
|
|
127
131
|
const rr = await regradeScenario({
|
|
128
132
|
runDir: column.runDir, spec, scenario, adapter, judge: results.judge,
|
|
129
|
-
specDir: dirname(specPath), threshold,
|
|
133
|
+
specDir: dirname(specPath), threshold, mode: results.mode,
|
|
130
134
|
});
|
|
131
135
|
const merged = results.scenarios.map((s) => s.id === body.scenarioId ? { ...rr, override: s.override, note: s.note } : s);
|
|
132
136
|
const written = writeResults(column.runDir, {
|
|
133
137
|
skill: results.skill, harness: results.harness, model: results.model, judge: results.judge,
|
|
134
138
|
timestamp: results.timestamp, label: results.label, mode: results.mode, scenarios: merged,
|
|
135
|
-
|
|
139
|
+
partial: results.partial,
|
|
140
|
+
// Provenance survives a UI re-judge, same as it does through `grade`.
|
|
141
|
+
harness_cli_version: results.harness_cli_version, delivery_canary: results.delivery_canary,
|
|
142
|
+
// Recorded hashes were being dropped here entirely, which silently
|
|
143
|
+
// retired the staleness gate for any run re-judged from the UI. Carried,
|
|
144
|
+
// with the one `rubric:` key this re-judge actually applied refreshed —
|
|
145
|
+
// the same doctrine `grade` follows (see refreshRubricHashes).
|
|
146
|
+
source_hashes: refreshRubricHashes(results.source_hashes, spec, [body.scenarioId]),
|
|
147
|
+
}, scoreContextFor(results, spec));
|
|
136
148
|
ensureResultsGitignore(join(opts.skillDir, "tests", "results"));
|
|
137
149
|
const g = written.effective_grade;
|
|
138
150
|
appendJournal(column.runDir, { event: "score", ts: new Date().toISOString(), passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note });
|
|
@@ -167,11 +179,11 @@ export async function serveReview(opts) {
|
|
|
167
179
|
return;
|
|
168
180
|
}
|
|
169
181
|
// writeResults recomputes effective_grade override-aware against the CURRENT
|
|
170
|
-
// spec's ship bar — a saved override can never leave a stale grade.
|
|
171
|
-
//
|
|
182
|
+
// spec's ship bar — a saved override can never leave a stale grade. Scored
|
|
183
|
+
// modes only: /save must not put a grade on a red baseline (PR #1 finding),
|
|
184
|
+
// and since 0.5.0 "scored" includes force.
|
|
172
185
|
const spec = loadSpec(join(opts.skillDir, "tests", "specification.yaml"));
|
|
173
|
-
|
|
174
|
-
writeResults(column.runDir, patched, ctx);
|
|
186
|
+
writeResults(column.runDir, patched, scoreContextFor(patched, spec));
|
|
175
187
|
// Unconditional: a results root created before schema-2/journal.jsonl existed
|
|
176
188
|
// may still have a stale .gitignore body — every save (not just overrides)
|
|
177
189
|
// must roll it forward so journal.jsonl doesn't end up tracked.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0",
|
|
48
|
+
"@skill-harness/adapters": "0.5.0"
|
|
49
49
|
}
|
|
50
50
|
}
|