@skill-harness/cli 0.4.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.grade.js +3 -1
- package/assets/report.template.html +25 -9
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +97 -13
- 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;
|
|
@@ -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; }
|
|
@@ -126,10 +128,10 @@ function render() {
|
|
|
126
128
|
const grades = DATA.columns.map((col) => gradeColumn(col, DATA.shipBar, DATA.critical));
|
|
127
129
|
const queue = [];
|
|
128
130
|
DATA.columns.forEach((col) => {
|
|
129
|
-
// A suspect cell in a red
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
if (col.mode && col.mode !== "green") return;
|
|
131
|
+
// A suspect cell in a red column can't be re-judged from here (/rejudge 400s
|
|
132
|
+
// for an unscored baseline) — don't queue it. The matrix below still shows
|
|
133
|
+
// suspect badges for every column regardless of mode.
|
|
134
|
+
if (col.mode && col.mode !== "green" && col.mode !== "force") return;
|
|
133
135
|
for (const scn of DATA.scenarios) {
|
|
134
136
|
const cell = col.cells[scn.id];
|
|
135
137
|
if (cell && cell.suspect && !cell.override) queue.push({ col: col.index, id: scn.id, label: col.label, reason: cell.judge_reason });
|
|
@@ -150,8 +152,8 @@ function render() {
|
|
|
150
152
|
DATA.columns.forEach((col, i) => {
|
|
151
153
|
const g = grades[i];
|
|
152
154
|
let gradeHtml;
|
|
153
|
-
if (col.mode && col.mode !== "green") {
|
|
154
|
-
//
|
|
155
|
+
if (col.mode && col.mode !== "green" && col.mode !== "force") {
|
|
156
|
+
// Green and force are scored; a red baseline column has no ship grade.
|
|
155
157
|
gradeHtml = `<span class='badge no'>not scored (${escapeHtml(col.mode)})</span>`;
|
|
156
158
|
if (g.suspect > 0) gradeHtml += ` — ${g.suspect} suspect`;
|
|
157
159
|
} else {
|
|
@@ -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
|
}
|
|
@@ -244,9 +254,10 @@ async function openPanel(colIndex, scenarioId) {
|
|
|
244
254
|
panel.innerHTML = `
|
|
245
255
|
<button class="close" id="closeBtn">×</button>
|
|
246
256
|
<h2>${scn.id} · ${escapeHtml(scn.title)}</h2>
|
|
247
|
-
<div class="meta">${escapeHtml(col.label)} · judge ${escapeHtml(col.judge.provider + ":" + col.judge.model)}</div>
|
|
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>
|
|
@@ -358,7 +369,12 @@ function renderTrends(data) {
|
|
|
358
369
|
const badge = last.ship ? "<span class='badge ship'>SHIP</span>" : "<span class='badge no'>NOT READY</span>";
|
|
359
370
|
const trunc = m.truncated ? ` <span class='dim'>(last ${m.runs.length})</span>` : "";
|
|
360
371
|
const skippedNote = m.skipped > 0 ? ` <span class='dim'>(${m.skipped} unreadable)</span>` : "";
|
|
361
|
-
|
|
372
|
+
// The mode is part of the series identity, not decoration: a tag that moved
|
|
373
|
+
// from green to force delivery has TWO series here, and the same skill text
|
|
374
|
+
// scores differently under each — so an unlabelled pair of sparklines would
|
|
375
|
+
// read as one history that jumped.
|
|
376
|
+
const modeNote = m.mode ? ` <span class='dim'>${escapeHtml(m.mode)}</span>` : "";
|
|
377
|
+
html += `<div class="tmodel"><div class="tmodel-h">${escapeHtml(m.model)}${modeNote} — ${sparkline(m.runs)} ${last.letter} (${last.pct}%) ${badge}${trunc}${skippedNote}</div>`;
|
|
362
378
|
html += "<table class='tgrid'><thead><tr><th></th>";
|
|
363
379
|
for (const run of m.runs) html += `<th title="${escapeHtml(run.label || run.timestamp)}">${escapeHtml((run.label || run.timestamp).slice(0, 8))}</th>`;
|
|
364
380
|
html += "</tr></thead><tbody>";
|
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";
|
|
@@ -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);
|
|
@@ -181,7 +183,11 @@ export async function cmdRun(args) {
|
|
|
181
183
|
// any earlier run — the tag dir (<harness>-<modelslug>) is the join key.
|
|
182
184
|
const tag = basename(dirname(summary.runDir));
|
|
183
185
|
const lift = collectLift(skill.dir).find((l) => l.tag === tag);
|
|
184
|
-
|
|
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");
|
|
185
191
|
}
|
|
186
192
|
}
|
|
187
193
|
console.log(`\nReview interactively: skill-harness review ${skills[0]?.name ?? "<skill>"} --skills ${root}`);
|
|
@@ -300,6 +306,68 @@ export async function cmdRegate(args, adapterOverride) {
|
|
|
300
306
|
// flipped reps, which it must not spend silently.
|
|
301
307
|
console.log(`\n${runDirs.length} run(s) re-gated, ${moved} verdict(s) moved, ${calls} judge call(s) (no model re-runs).`);
|
|
302
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
|
+
}
|
|
303
371
|
async function cmdReview(args) {
|
|
304
372
|
const root = flagStr(args, "skills", process.cwd());
|
|
305
373
|
const target = args._[0];
|
|
@@ -433,7 +501,15 @@ export async function cmdSuggest(args, adapterOverride) {
|
|
|
433
501
|
rmSync(cwd, { recursive: true, force: true });
|
|
434
502
|
}
|
|
435
503
|
}
|
|
436
|
-
/**
|
|
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
|
+
*/
|
|
437
513
|
export async function cmdLint(args) {
|
|
438
514
|
const root = flagStr(args, "skills", process.cwd());
|
|
439
515
|
const target = args._[0] ?? "all";
|
|
@@ -464,18 +540,20 @@ export async function cmdLint(args) {
|
|
|
464
540
|
f = [{ skill: dir, code: "lint-error", message: e instanceof Error ? e.message : String(e) }];
|
|
465
541
|
}
|
|
466
542
|
findings.push(...f);
|
|
467
|
-
if (f.length === 0)
|
|
543
|
+
if (f.filter(failsGate).length === 0)
|
|
468
544
|
console.log(`✓ ${dir}`);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
+
}
|
|
476
552
|
}
|
|
477
|
-
|
|
478
|
-
|
|
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;
|
|
479
557
|
}
|
|
480
558
|
// ---------------------------------------------------------------- dispatch
|
|
481
559
|
/**
|
|
@@ -491,9 +569,11 @@ export function help() {
|
|
|
491
569
|
|
|
492
570
|
run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
|
|
493
571
|
[--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
|
|
572
|
+
[--canary] green only: spend ONE probe proving the skill reached the model, and abort the run if it did not
|
|
494
573
|
grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
|
|
495
574
|
rescore <run-dir>... re-score saved reps vs current spec thresholds (free)
|
|
496
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)
|
|
497
577
|
review <skill> --skills <root> [--port N] serve the interactive review UI
|
|
498
578
|
add-test <skill> --skills <root> --id ID --title T --turn ... --check ... [--critical] [--mode seeded --fixture path]
|
|
499
579
|
init <skill> --skills <root> [--force] scaffold a commented template spec (free, offline)
|
|
@@ -504,6 +584,9 @@ export function help() {
|
|
|
504
584
|
version print ${HARNESS_VERSION} and exit (also --version / -v)
|
|
505
585
|
|
|
506
586
|
defaults: model=${DEFAULT_MODEL} judge=${defaultJudge()} mode=green harness=pi
|
|
587
|
+
green and force are both scored; red is the unscored baseline. green delivery depends on the
|
|
588
|
+
harness version (pi >= 0.83.0 discloses only the skill's description and loads the body on demand),
|
|
589
|
+
so --mode force is the delivery that cannot silently degrade — and --canary proves green per run.
|
|
507
590
|
the judge default is Opus on your Claude subscription (\`claude-code\` → \`claude -p\`), not a
|
|
508
591
|
metered API key. Set SKILL_HARNESS_JUDGE to change it for a repo or a shell; --judge wins over both.`;
|
|
509
592
|
}
|
|
@@ -515,6 +598,7 @@ export async function main(argv) {
|
|
|
515
598
|
case "grade": return cmdGrade(args);
|
|
516
599
|
case "rescore": return cmdRescore(args);
|
|
517
600
|
case "regate": return cmdRegate(args);
|
|
601
|
+
case "stability": return cmdStability(args);
|
|
518
602
|
case "review": return cmdReview(args);
|
|
519
603
|
case "add-test": return cmdAddTest(args);
|
|
520
604
|
case "init": return cmdInit(args);
|
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.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
|
}
|