@skill-harness/cli 0.11.0 → 0.12.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 +1 -0
- package/assets/report.grade.js +11 -4
- package/assets/report.template.html +4 -2
- package/dist/cli.d.ts +5 -3
- package/dist/cli.js +56 -17
- package/dist/mutation-catalogue.d.ts +8 -0
- package/dist/mutation-catalogue.js +95 -0
- package/dist/qualification.d.ts +7 -0
- package/dist/qualification.js +210 -0
- package/dist/serve.js +26 -11
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ npm i -g @skill-harness/cli
|
|
|
24
24
|
| `skill-harness grade <run-dir> [--judge p:m]` | re-grade saved transcripts |
|
|
25
25
|
| `skill-harness review <skill> --skills <root> [--port N]` | serve the interactive review UI |
|
|
26
26
|
| `skill-harness add-test <skill> --skills <root> --id ID --title T ...` | scaffold a new scenario |
|
|
27
|
+
| `skill-harness qualification prepare\|start\|status\|poll\|validate\|abort ...` | durable external-config qualification lifecycle |
|
|
27
28
|
|
|
28
29
|
## More
|
|
29
30
|
|
package/assets/report.grade.js
CHANGED
|
@@ -28,6 +28,7 @@ export function effective(cell) {
|
|
|
28
28
|
// Mechanical evidence outranks a prose judge. Objective PASS deliberately
|
|
29
29
|
// forces nothing — the checklist judge still decides the behavioral rubric.
|
|
30
30
|
if (cell.objective && cell.objective.status === "ERROR") return "ERROR";
|
|
31
|
+
if (cell.objective && cell.objective.status === "NOT-MEASURED") return "NOT-MEASURED";
|
|
31
32
|
if (cell.objective && cell.objective.status === "FAIL") return "FAIL";
|
|
32
33
|
return cell.judge_verdict;
|
|
33
34
|
}
|
|
@@ -53,6 +54,7 @@ export function gradeColumn(col, shipBar, critical) {
|
|
|
53
54
|
let bFails = 0;
|
|
54
55
|
let suspect = 0;
|
|
55
56
|
let errors = 0;
|
|
57
|
+
let notMeasured = 0;
|
|
56
58
|
|
|
57
59
|
for (const id of Object.keys(col.cells)) {
|
|
58
60
|
const cell = col.cells[id];
|
|
@@ -62,6 +64,10 @@ export function gradeColumn(col, shipBar, critical) {
|
|
|
62
64
|
continue; // excluded, blocks ship
|
|
63
65
|
}
|
|
64
66
|
const verdict = effective(cell);
|
|
67
|
+
if (verdict === "NOT-MEASURED") {
|
|
68
|
+
notMeasured++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
65
71
|
if (verdict === "ERROR" || verdict === "JUDGE-AMBIGUOUS") {
|
|
66
72
|
errors++;
|
|
67
73
|
continue;
|
|
@@ -76,7 +82,7 @@ export function gradeColumn(col, shipBar, critical) {
|
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
if (col.partial === true) {
|
|
79
|
-
return { passed: 0, total: 0, pct: 0, letter: "-", ship: false, criticalFails, bFails, suspect, errors };
|
|
85
|
+
return { passed: 0, total: 0, pct: 0, letter: "-", ship: false, criticalFails, bFails, suspect, errors, notMeasured };
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
const pct = total > 0 ? Math.round((passed * 100) / total) : 0;
|
|
@@ -89,9 +95,10 @@ export function gradeColumn(col, shipBar, critical) {
|
|
|
89
95
|
(!shipBar.no_critical_fail || criticalFails === 0) &&
|
|
90
96
|
bFails === 0 &&
|
|
91
97
|
suspect === 0 &&
|
|
92
|
-
errors === 0
|
|
98
|
+
errors === 0 &&
|
|
99
|
+
notMeasured === 0;
|
|
93
100
|
|
|
94
|
-
return { passed, total, pct, letter, ship, criticalFails, bFails, suspect, errors };
|
|
101
|
+
return { passed, total, pct, letter, ship, criticalFails, bFails, suspect, errors, notMeasured };
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
/**
|
|
@@ -107,7 +114,7 @@ export function gradeColumn(col, shipBar, critical) {
|
|
|
107
114
|
*/
|
|
108
115
|
export function liftClass(liftCell, greenCell) {
|
|
109
116
|
const conclusive = (verdict, suspect) =>
|
|
110
|
-
!suspect && verdict !== "ERROR" && verdict !== "JUDGE-AMBIGUOUS";
|
|
117
|
+
!suspect && verdict !== "ERROR" && verdict !== "NOT-MEASURED" && verdict !== "JUDGE-AMBIGUOUS";
|
|
111
118
|
|
|
112
119
|
const redOk = conclusive(liftCell.red, liftCell.redSuspect);
|
|
113
120
|
const greenOk = conclusive(effective(greenCell), !!greenCell.suspect && !greenCell.override);
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
.cell.PASS { background: rgba(31,139,76,.22); }
|
|
26
26
|
.cell.FAIL { background: rgba(192,57,43,.24); }
|
|
27
27
|
.cell.ERROR { background: rgba(107,114,128,.22); }
|
|
28
|
+
.cell.NOT-MEASURED { background: rgba(180,83,9,.18); }
|
|
28
29
|
.cell.empty { color: var(--dim); cursor: default; font-weight: 400; }
|
|
29
30
|
.cell .ov { display: block; font-size: 11px; color: var(--over); font-weight: 600; }
|
|
30
31
|
.cell .ov.unsaved { color: #b45309; font-style: italic; }
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
amber rather than red — missing evidence is not a failing gate. */
|
|
50
51
|
.cell .lm.obj-pass { color: var(--pass); }
|
|
51
52
|
.cell .lm.obj-fail { color: var(--fail); }
|
|
52
|
-
.cell .lm.obj-error { color: #b45309; }
|
|
53
|
+
.cell .lm.obj-error, .cell .lm.obj-not-measured { color: #b45309; }
|
|
53
54
|
/* Adjudication. `unresolved` is amber for the same reason `suspect` is: it must
|
|
54
55
|
never read as a clean PASS or FAIL. */
|
|
55
56
|
.cell .lm.adj-settled { color: var(--dim); }
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
.tgrid td.tc.PASS { color: #16a34a; }
|
|
95
96
|
.tgrid td.tc.FAIL { color: #dc2626; }
|
|
96
97
|
.tgrid td.tc.ERROR { color: #a855f7; }
|
|
98
|
+
.tgrid td.tc.NOT-MEASURED { color: #b45309; }
|
|
97
99
|
.tgrid td.tc.suspect { color: #b45309; }
|
|
98
100
|
.tgrid td.tc.absent { color: var(--dim, #bbb); }
|
|
99
101
|
.dim { color: var(--dim, #888); font-weight: 400; }
|
|
@@ -429,7 +431,7 @@ function sparkline(runs) {
|
|
|
429
431
|
function renderTrends(data) {
|
|
430
432
|
const box = document.getElementById("trends");
|
|
431
433
|
if (!data.models.length) { box.innerHTML = `<div class="empty-state">No runs yet.</div>`; return; }
|
|
432
|
-
const glyph = { PASS: "✓", FAIL: "✗", ERROR: "!" };
|
|
434
|
+
const glyph = { PASS: "✓", FAIL: "✗", ERROR: "!", "NOT-MEASURED": "∅" };
|
|
433
435
|
let html = "";
|
|
434
436
|
for (const m of data.models) {
|
|
435
437
|
const last = m.runs[m.runs.length - 1].grade;
|
package/dist/cli.d.ts
CHANGED
|
@@ -30,13 +30,15 @@ export declare function cmdGrade(args: Args, adapterOverride?: HarnessAdapter):
|
|
|
30
30
|
* Reps are the measurement; thresholds are policy. When policy changes, recompute rather
|
|
31
31
|
* than reconcile two numbers in prose.
|
|
32
32
|
*/
|
|
33
|
+
export declare function cmdJudgeAgreement(args: Args): void;
|
|
33
34
|
export declare function cmdMutationTest(): Promise<void>;
|
|
34
35
|
/**
|
|
35
|
-
* Re-evaluate needle gates against the saved staged diffs —
|
|
36
|
-
* whose gate verdict flips from fail to pass
|
|
37
|
-
*
|
|
36
|
+
* Re-evaluate needle gates against the saved staged diffs — no subject call. A rep
|
|
37
|
+
* whose gate verdict flips from fail to pass was never judged and costs one judge call.
|
|
38
|
+
* The completion summary reports how many judge calls were made.
|
|
38
39
|
*/
|
|
39
40
|
export declare function cmdRegate(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
|
|
41
|
+
export declare function cmdScreen(args: Args): Promise<void>;
|
|
40
42
|
export declare function cmdInit(args: Args): Promise<void>;
|
|
41
43
|
export declare function cmdSuggest(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
|
|
42
44
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,12 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync
|
|
|
3
3
|
import { load as yamlLoad } from "js-yaml";
|
|
4
4
|
import { basename, dirname, join, resolve, relative } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { discover, resolveSkill, loadSpec, parseSpec, appendScenario, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, failsGate, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, regateRun, specPathForRunDir, collectLift, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, restampSkill, resolveAdjudicationJudges, adjudicateRun, judgeResemblesSubject, computeCoverage, formatCoverage, selectAffected, formatAffected, gitDiff, exec, HARNESS_VERSION, defaultJudge, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, isScoredMode, runTrajectoryMutationSelfTest, resolveArm, } from "@skill-harness/core";
|
|
6
|
+
import { discover, resolveSkill, loadSpec, parseSpec, appendScenario, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, failsGate, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, regateRun, specPathForRunDir, collectLift, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, restampSkill, resolveAdjudicationJudges, adjudicateRun, judgeResemblesSubject, computeCoverage, formatCoverage, selectAffected, formatAffected, gitDiff, exec, HARNESS_VERSION, defaultJudge, assertJudgeAllowed, judgeAgreement, assertNotDowngraded, downgradeWarning, isScoredMode, runTrajectoryMutationSelfTest, resolveArm, isFreeOfflineCommand, screenResults, formatScreen, } from "@skill-harness/core";
|
|
7
7
|
import { getAdapter } from "@skill-harness/adapters";
|
|
8
8
|
import { serveReview } from "./serve.js";
|
|
9
|
+
import { runSelfScreeningMutationCases } from "./mutation-catalogue.js";
|
|
9
10
|
import { runCompareCommand } from "./compare.js";
|
|
11
|
+
import { cmdQualification } from "./qualification.js";
|
|
10
12
|
const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
|
|
11
13
|
// The judge default lives in core (`defaultJudge()`), which resolves
|
|
12
14
|
// SKILL_HARNESS_JUDGE over a baked value — it was duplicated in three places
|
|
@@ -388,9 +390,24 @@ export async function cmdGrade(args, adapterOverride) {
|
|
|
388
390
|
* Reps are the measurement; thresholds are policy. When policy changes, recompute rather
|
|
389
391
|
* than reconcile two numbers in prose.
|
|
390
392
|
*/
|
|
393
|
+
export function cmdJudgeAgreement(args) {
|
|
394
|
+
const raw = args._[0];
|
|
395
|
+
if (!raw)
|
|
396
|
+
throw new Error("usage: skill-harness judge-agreement <run-dir>");
|
|
397
|
+
const runDir = resolve(raw);
|
|
398
|
+
if (!existsSync(runDir))
|
|
399
|
+
throw new Error(`run dir not found: ${runDir}`);
|
|
400
|
+
const report = judgeAgreement(readResults(runDir));
|
|
401
|
+
for (const cell of report.cells) {
|
|
402
|
+
console.log(` ${cell.status === "agree" ? "=" : cell.status === "disagree" ? "≠" : "?"} ${cell.scenario}: ${cell.status} — ${cell.detail}${cell.judges.length ? ` (${cell.judges.join(" vs ")})` : ""}`);
|
|
403
|
+
}
|
|
404
|
+
console.log(`\njudge agreement: ${report.agree} agree / ${report.disagree} disagree / ${report.error} error; ${report.rate === null ? "n/a" : `${(report.rate * 100).toFixed(1)}%`} across ${report.comparable} comparable scenario(s).`);
|
|
405
|
+
console.log("offline report only; no model or judge calls.");
|
|
406
|
+
}
|
|
391
407
|
export async function cmdMutationTest() {
|
|
392
|
-
const
|
|
393
|
-
|
|
408
|
+
const trajectory = runTrajectoryMutationSelfTest();
|
|
409
|
+
const report = { baseline: trajectory.baseline, cases: [...trajectory.cases, ...await runSelfScreeningMutationCases()] };
|
|
410
|
+
console.log(`mutation self-test: baseline ${report.baseline}`);
|
|
394
411
|
for (const test of report.cases) {
|
|
395
412
|
console.log(` ${test.detected ? "✓" : "✗"} ${test.id}: ${test.status} — ${test.detail}`);
|
|
396
413
|
}
|
|
@@ -423,9 +440,9 @@ async function cmdRescore(args) {
|
|
|
423
440
|
console.log(`\n${runDirs.length} run(s) re-scored, ${moved} verdict(s) moved.`);
|
|
424
441
|
}
|
|
425
442
|
/**
|
|
426
|
-
* Re-evaluate needle gates against the saved staged diffs —
|
|
427
|
-
* whose gate verdict flips from fail to pass
|
|
428
|
-
*
|
|
443
|
+
* Re-evaluate needle gates against the saved staged diffs — no subject call. A rep
|
|
444
|
+
* whose gate verdict flips from fail to pass was never judged and costs one judge call.
|
|
445
|
+
* The completion summary reports how many judge calls were made.
|
|
429
446
|
*/
|
|
430
447
|
export async function cmdRegate(args, adapterOverride) {
|
|
431
448
|
const runDirs = args._;
|
|
@@ -462,7 +479,7 @@ export async function cmdRegate(args, adapterOverride) {
|
|
|
462
479
|
moved += changes.length;
|
|
463
480
|
calls += judgeCalls;
|
|
464
481
|
}
|
|
465
|
-
// The cost line matters: regate
|
|
482
|
+
// The cost line matters: regate makes no subject call, but fail→pass reps require a judge.
|
|
466
483
|
// flipped reps, which it must not spend silently.
|
|
467
484
|
console.log(`\n${runDirs.length} run(s) re-gated, ${moved} verdict(s) moved, ${calls} judge call(s) (no model re-runs).`);
|
|
468
485
|
}
|
|
@@ -517,6 +534,13 @@ async function cmdRestamp(args) {
|
|
|
517
534
|
console.log(`\n${runs} run(s) examined: ${upgraded} upgraded, ${unprovable} left alone, ${unchanged} already current.` +
|
|
518
535
|
`${partial > 0 ? ` (${partial} upgraded only in part.)` : ""} No models were called.`);
|
|
519
536
|
}
|
|
537
|
+
export async function cmdScreen(args) {
|
|
538
|
+
if (args._.length === 0)
|
|
539
|
+
throw new Error("usage: skill-harness screen <run-dir> [<run-dir> ...]");
|
|
540
|
+
const results = args._.map(runDir => readResults(resolve(runDir)));
|
|
541
|
+
console.log(formatScreen(screenResults(results)));
|
|
542
|
+
console.log(`\n${results.length} retained result(s) screened; 0 subject calls, 0 judge calls.`);
|
|
543
|
+
}
|
|
520
544
|
async function cmdStability(args) {
|
|
521
545
|
const root = flagStr(args, "skills", process.cwd());
|
|
522
546
|
const target = args._[0] ?? "all";
|
|
@@ -860,6 +884,11 @@ export async function cmdLint(args) {
|
|
|
860
884
|
* prints none.
|
|
861
885
|
*/
|
|
862
886
|
export function help() {
|
|
887
|
+
const free = (command) => {
|
|
888
|
+
if (!isFreeOfflineCommand(command))
|
|
889
|
+
throw new Error(`${command} is not a free/offline command`);
|
|
890
|
+
return "free, offline";
|
|
891
|
+
};
|
|
863
892
|
return `skill-harness ${HARNESS_VERSION} — test/optimize loop for agent skills (pi harness)
|
|
864
893
|
|
|
865
894
|
run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
|
|
@@ -877,19 +906,26 @@ export function help() {
|
|
|
877
906
|
[--auto-rejudge] [--secondary-judge p:m] [--tie-break-judge p:m]
|
|
878
907
|
ask again about untrustworthy cells (ambiguous / contradictory / non-unanimous /
|
|
879
908
|
ship-deciding). OFF by default; prints the exact MAX extra call count first.
|
|
880
|
-
mutation-test prove trajectory
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
909
|
+
mutation-test prove trajectory + results/delivery/screen gates turn red (${free("mutation-test")})
|
|
910
|
+
judge-agreement <run-dir> compare two distinct persisted judge votes per scenario (${free("judge-agreement")})
|
|
911
|
+
rescore <run-dir>... re-score saved reps vs current spec thresholds (${free("rescore")})
|
|
912
|
+
regate <run-dir>... [--judge prov:model] re-evaluate saved gates (no subject call; judges fail→pass reps)
|
|
913
|
+
restamp <skill|all> --skills <root> [--from <git-ref>] record the model-visible skill digest on runs that still match (${free("restamp")}; one-time migration)
|
|
914
|
+
stability <skill|all> --skills <root> [--window N] [--all] run-over-run verdict flips per scenario (${free("stability")})
|
|
915
|
+
screen <run-dir>... retained control/treatment + criterion rates (${free("screen")})
|
|
885
916
|
review <skill> --skills <root> [--port N] serve the interactive review UI
|
|
886
917
|
add-test <skill> --skills <root> --id ID --title T --turn ... --check ... [--critical] [--mode seeded --fixture path]
|
|
887
|
-
init <skill> --skills <root> [--force] scaffold a commented template spec (free
|
|
918
|
+
init <skill> --skills <root> [--force] scaffold a commented template spec (${free("init")})
|
|
888
919
|
suggest <skill> --skills <root> [--model prov:model] [--force] LLM-draft a spec from SKILL.md (spends tokens)
|
|
889
|
-
list --skills <root> discovered skills + spec status
|
|
890
|
-
lint <skill|all> --skills <root> validate specs/fixtures + results-consistency (CI gate; exits non-zero on findings)
|
|
891
|
-
coverage <skill|all> --skills <root> [--strict] which instruction sections have a declared test (free
|
|
892
|
-
affected <skill> --skills <root> [--base ref] which scenarios a change could touch (free
|
|
920
|
+
list --skills <root> discovered skills + spec status (${free("list")})
|
|
921
|
+
lint <skill|all> --skills <root> validate specs/fixtures + results-consistency (${free("lint")}; CI gate; exits non-zero on findings)
|
|
922
|
+
coverage <skill|all> --skills <root> [--strict] which instruction sections have a declared test (${free("coverage")})
|
|
923
|
+
affected <skill> --skills <root> [--base ref] which scenarios a change could touch (${free("affected")})
|
|
924
|
+
qualification <prepare|start|status|poll|validate|panel|cell|abort> durable qualification-runner-v1 lifecycle + offline panel collapse
|
|
925
|
+
prepare --spool DIR --config FILE --request FILE [--expected-config-sha256 HEX] (required in production)
|
|
926
|
+
start|status|poll|abort --spool DIR --id ID (abort also requires --reason ID)
|
|
927
|
+
start may resume only with --continuation-authority-file <private-0600-prebound-one-use-capability>
|
|
928
|
+
validate --spool DIR (all calls use external, schema-validated arms; no automatic retry)
|
|
893
929
|
|
|
894
930
|
version print ${HARNESS_VERSION} and exit (also --version / -v)
|
|
895
931
|
|
|
@@ -908,10 +944,12 @@ export async function main(argv) {
|
|
|
908
944
|
case "compare": return cmdCompare(args);
|
|
909
945
|
case "grade": return cmdGrade(args);
|
|
910
946
|
case "mutation-test": return cmdMutationTest();
|
|
947
|
+
case "judge-agreement": return cmdJudgeAgreement(args);
|
|
911
948
|
case "rescore": return cmdRescore(args);
|
|
912
949
|
case "regate": return cmdRegate(args);
|
|
913
950
|
case "restamp": return cmdRestamp(args);
|
|
914
951
|
case "stability": return cmdStability(args);
|
|
952
|
+
case "screen": return cmdScreen(args);
|
|
915
953
|
case "review": return cmdReview(args);
|
|
916
954
|
case "add-test": return cmdAddTest(args);
|
|
917
955
|
case "init": return cmdInit(args);
|
|
@@ -920,6 +958,7 @@ export async function main(argv) {
|
|
|
920
958
|
case "lint": return cmdLint(args);
|
|
921
959
|
case "coverage": return cmdCoverage(args);
|
|
922
960
|
case "affected": return cmdAffected(args);
|
|
961
|
+
case "qualification": return cmdQualification(args);
|
|
923
962
|
case "version":
|
|
924
963
|
case "--version":
|
|
925
964
|
case "-v":
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PermanentMutationCase {
|
|
2
|
+
id: string;
|
|
3
|
+
detected: boolean;
|
|
4
|
+
status: "FAIL" | "ERROR";
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
/** Permanent non-trajectory mutation cases exercised by the free mutation-test command. */
|
|
8
|
+
export declare function runSelfScreeningMutationCases(): Promise<PermanentMutationCase[]>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseSpec, planAdjudication, runSkillModel, screenResults, validateResults, } from "@skill-harness/core";
|
|
5
|
+
import { authenticatePromptObservation, bindPromptObservation, normalizePromptPayload, observeProviderPayload, promptCaptureIsTrusted, } from "@skill-harness/adapters";
|
|
6
|
+
const h = "a".repeat(64);
|
|
7
|
+
const deliveryAssertion = (status = "PASS") => ({ kind: "skill_delivered", status, detail: status });
|
|
8
|
+
function validV3() {
|
|
9
|
+
return {
|
|
10
|
+
schema: 3, skill: "mutation", harness: "pi", model: "fake:m", judge: { provider: "fake", model: "j" },
|
|
11
|
+
timestamp: "t", label: null, mode: "force", effective_grade: { passed: 1, total: 1, pct: 100, letter: "A", ship: true, note: "" },
|
|
12
|
+
subject_invocations: [{ scenario_id: "A1", repetition: 0, prompt: { capture_version: "prompt-provenance-v1", request_index: 0, raw_sha256: h, normalized_sha256: h, normalization_rule: "cwd-line-v1", bytes: 1, contract_sha256: h, contract_bytes: 1, contract_occurrences: 1, mechanism: "append-system-prompt", status: "PASS" } }],
|
|
13
|
+
scenarios: [{ id: "A1", criterion_count: 1, judge_verdict: "PASS", judge_reason: "ok", suspect: false, override: null, note: "", objective: { status: "PASS", assertions: [deliveryAssertion()] }, rep_judgments: [{ repetition: 0, recorded_verdict: "PASS", objective: { status: "PASS", assertions: [deliveryAssertion()] }, judgments: [{ ordinal: 1, judge: { provider: "fake", model: "j" }, verdict: "PASS", reason: "ok", suspect: false, criteria: [{ index: 1, verdict: "PASS", reason: "ok" }] }] }] }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function catches(id, mutate, detail) {
|
|
17
|
+
const result = structuredClone(validV3());
|
|
18
|
+
mutate(result);
|
|
19
|
+
let detected = false;
|
|
20
|
+
try {
|
|
21
|
+
validateResults(result);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
detected = true;
|
|
25
|
+
}
|
|
26
|
+
return { id, detected, status: detected ? "FAIL" : "ERROR", detail };
|
|
27
|
+
}
|
|
28
|
+
function screenFile(id, verdicts) {
|
|
29
|
+
const result = validV3();
|
|
30
|
+
result.scenarios[0].id = id;
|
|
31
|
+
result.scenarios[0].reps = verdicts.length;
|
|
32
|
+
result.subject_invocations = verdicts.map((_, repetition) => ({ ...structuredClone(result.subject_invocations[0]), scenario_id: id, repetition }));
|
|
33
|
+
result.scenarios[0].rep_judgments = verdicts.map((verdict, repetition) => ({ repetition, recorded_verdict: verdict, objective: { status: "PASS", assertions: [deliveryAssertion()] }, judgments: [{ ordinal: 1, judge: { provider: "fake", model: "j" }, verdict, reason: verdict, suspect: false, criteria: [{ index: 1, verdict, reason: verdict }] }] }));
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
async function objectiveSuppressesJudge() {
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), "skill-harness-mutation-delivery-"));
|
|
38
|
+
let judgeCalls = 0;
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(join(dir, "tests"), { recursive: true });
|
|
41
|
+
writeFileSync(join(dir, "SKILL.md"), "---\nname: mutation\ndescription: fixture\n---\n\n## Contract\nDo it.\n", "utf8");
|
|
42
|
+
const specPath = join(dir, "tests/specification.yaml");
|
|
43
|
+
writeFileSync(specPath, "skill: mutation\njudge_persona: strict\nship_bar: { total: 1, min_pass: 1, no_critical_fail: true }\ncritical: []\nscenarios:\n - id: A1\n title: delivery\n turns: [hi]\n checklist: [does it]\n", "utf8");
|
|
44
|
+
const spec = parseSpec(readFileSync(specPath, "utf8"), specPath);
|
|
45
|
+
const adapter = {
|
|
46
|
+
name: "pi", observesPrompts: true, available: async () => true,
|
|
47
|
+
run: async (req) => { req.onPromptObservation?.({ capture_version: "prompt-provenance-v1", request_index: 0, raw_sha256: h, normalized_sha256: h, normalization_rule: "cwd-line-v1", bytes: 1, contract_sha256: h, contract_bytes: 1, contract_occurrences: 0, mechanism: "append-system-prompt", status: "NOT-MEASURED" }); return ">>> USER:\nhi\n\n<<< ASSISTANT:\nanswer\n"; },
|
|
48
|
+
judge: async () => { judgeCalls++; return "VERDICT: PASS\nREASON: wrong"; },
|
|
49
|
+
};
|
|
50
|
+
const out = await runSkillModel({ spec, skillDir: dir, specPath, adapter, model: { provider: "fake", model: "m" }, modelToken: "fake:m", judge: { provider: "fake-judge", model: "j" }, mode: "force", timestamp: "2026-09-04T01:00:00.000Z" });
|
|
51
|
+
const detected = judgeCalls === 0 && out.results.scenarios[0].judge_verdict === "NOT-MEASURED" && out.results.effective_grade.total === 0;
|
|
52
|
+
return { id: "delivery-objective-suppresses-judge", detected, status: detected ? "FAIL" : "ERROR", detail: "undelivered subject must never reach the judge or efficacy denominator" };
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Permanent non-trajectory mutation cases exercised by the free mutation-test command. */
|
|
59
|
+
export async function runSelfScreeningMutationCases() {
|
|
60
|
+
const contract = "## Contract\nDo it\n";
|
|
61
|
+
const one = observeProviderPayload({ instructions: contract }, contract, "append-system-prompt", 0);
|
|
62
|
+
const forged = { ...one, contract_sha256: "f".repeat(64) };
|
|
63
|
+
const ceiling = screenFile("C", ["PASS", "PASS", "PASS", "PASS", "FAIL"]);
|
|
64
|
+
for (const observation of ceiling.subject_invocations)
|
|
65
|
+
observation.prompt = { ...observation.prompt, mechanism: "none", contract_occurrences: 0, status: "PASS" };
|
|
66
|
+
const undelivered = screenFile("U", ["PASS", "PASS"]);
|
|
67
|
+
undelivered.subject_invocations[1].prompt = { ...undelivered.subject_invocations[1].prompt, contract_occurrences: 0, status: "NOT-MEASURED" };
|
|
68
|
+
const suspect = screenFile("S", ["PASS"]);
|
|
69
|
+
suspect.scenarios[0].rep_judgments[0].judgments[0].suspect = true;
|
|
70
|
+
const cases = [
|
|
71
|
+
catches("schema-v3-missing-observation-rejected", r => { delete r.subject_invocations; }, "schema v3 must reject missing delivery evidence"),
|
|
72
|
+
catches("schema-v3-panel-divergence-rejected", r => { r.scenarios[0].rep_judgments[0].recorded_verdict = "FAIL"; }, "stored panel result must equal recomputed clean votes"),
|
|
73
|
+
catches("schema-v3-missing-criterion-rejected", r => { r.scenarios[0].rep_judgments[0].judgments[0].criteria.pop(); }, "criterion omissions must remain explicit"),
|
|
74
|
+
catches("schema-v3-per-repetition-objective-rejected", r => { delete r.scenarios[0].rep_judgments[0].objective; }, "aggregate objective cannot hide a repetition"),
|
|
75
|
+
catches("schema-v3-top-level-verdict-rejected", r => { const panel = r.scenarios[0].rep_judgments[0]; panel.judgments[0].verdict = "FAIL"; panel.judgments[0].criteria = panel.judgments[0].criteria.map(vote => ({ ...vote, verdict: "FAIL" })); panel.recorded_verdict = "FAIL"; }, "top-level verdict must match retained repetition evidence"),
|
|
76
|
+
catches("schema-v3-unsupported-pass-rejected", r => { r.scenarios[0].rep_judgments[0].judgments = []; }, "behavioral PASS requires a retained clean judgment"),
|
|
77
|
+
catches("schema-v3-adjudication-state-rejected", r => { const primary = r.scenarios[0].rep_judgments[0].judgments[0]; r.scenarios[0].adjudication = { repetition: 0, trigger: "ship_deciding", state: "unresolved", judgments: [primary, { ...structuredClone(primary), ordinal: 2 }] }; }, "adjudication state and verdict must match clean votes"),
|
|
78
|
+
{ id: "delivery-zero-not-measured", detected: observeProviderPayload({ instructions: "none" }, contract, "append-system-prompt", 0).status === "NOT-MEASURED", status: "FAIL", detail: "zero delivery is not product failure" },
|
|
79
|
+
{ id: "delivery-duplicate-not-measured", detected: observeProviderPayload({ instructions: contract + contract }, contract, "append-system-prompt", 0).status === "NOT-MEASURED", status: "FAIL", detail: "duplicate delivery is not valid measurement" },
|
|
80
|
+
{ id: "delivery-unobservable-error", detected: observeProviderPayload({}, contract, "append-system-prompt", 0).status === "ERROR", status: "FAIL", detail: "unsupported payload is instrument error" },
|
|
81
|
+
await objectiveSuppressesJudge(),
|
|
82
|
+
{ id: "delivery-error-suppresses-adjudication", detected: planAdjudication({ cells: [{ id: "A1", verdict: "ERROR", reason: "instrument", suspect: false, deliveryStatus: "ERROR" }], scenarios: [{ id: "A1", title: "A1", critical: false, mode: "inline", turns: ["x"], checklist: ["y"], workspace: "none", remote: false }], shipBar: { total: 1, min_pass: 1, no_critical_fail: true }, critical: [], tieBreakAvailable: true }).maxAdditionalCalls === 0, status: "FAIL", detail: "instrument delivery ERROR cannot authorize adjudication spend" },
|
|
83
|
+
{ id: "observer-normalization-scope", detected: JSON.stringify(normalizePromptPayload({ instructions: "Keep /a\nCurrent working directory: /a\nKeep /a" }, "cwd-line-v1")) === JSON.stringify({ instructions: "Keep /a\nCurrent working directory:<normalized>\nKeep /a" }), status: "FAIL", detail: "normalization changes only the cwd line" },
|
|
84
|
+
{ id: "observer-contract-binding", detected: bindPromptObservation(authenticatePromptObservation(forged, "key"), contract, "append-system-prompt", 0, "key").status === "ERROR", status: "FAIL", detail: "child contract identity cannot replace parent binding" },
|
|
85
|
+
{ id: "observer-mac-tamper-rejected", detected: bindPromptObservation({ ...authenticatePromptObservation(one, "key"), mac: "0".repeat(64) }, contract, "append-system-prompt", 0, "key").status === "ERROR", status: "FAIL", detail: "complete child observation requires parent authentication" },
|
|
86
|
+
{ id: "observer-extension-forgery-rejected", detected: !promptCaptureIsTrusted(1), status: "FAIL", detail: "same-process subject extensions cannot produce trusted provenance" },
|
|
87
|
+
{ id: "observer-argv-wiring", detected: promptCaptureIsTrusted(0) && !promptCaptureIsTrusted(1), status: "FAIL", detail: "observer is enabled only on the authenticated extension-free path" },
|
|
88
|
+
{ id: "screen-ceiling-boundary", detected: screenResults([ceiling]).scenarios[0].classification === "CEILING", status: "FAIL", detail: "four of five control passes is CEILING" },
|
|
89
|
+
{ id: "screen-undelivered-filter", detected: (() => { const row = screenResults([undelivered]).scenarios[0]; return row.treatment.n === 1 && row.not_measured === 1; })(), status: "FAIL", detail: "undelivered reps have a bucket outside efficacy" },
|
|
90
|
+
{ id: "screen-suspect-filter", detected: screenResults([suspect]).scenarios[0].control.n === 0, status: "FAIL", detail: "suspect-only panels do not enter rates" },
|
|
91
|
+
{ id: "screen-unsupported-pass-filter", detected: (() => { const unsupported = screenFile("P", ["PASS"]); unsupported.scenarios[0].rep_judgments[0].judgments = []; return screenResults([unsupported]).scenarios[0].control.n === 0; })(), status: "FAIL", detail: "recorded PASS without a clean vote does not enter efficacy" },
|
|
92
|
+
];
|
|
93
|
+
return cases.map(test => ({ ...test, status: test.detected ? "FAIL" : "ERROR" }));
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=mutation-catalogue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface QualificationCliArgs {
|
|
2
|
+
_: string[];
|
|
3
|
+
flags: Record<string, string | true>;
|
|
4
|
+
}
|
|
5
|
+
export declare function qualificationSupervisorRuntimeArgs(mode: "production" | "test", scriptPath: string): string[];
|
|
6
|
+
export declare function consumeQualificationContinuationAuthority(path: string): string;
|
|
7
|
+
export declare function cmdQualification(args: QualificationCliArgs): Promise<void>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import { abortQualificationInvocation, checkQualificationAuthentication, pollQualificationInvocation, prepareQualificationInvocation, qualificationCanonicalJson, recordQualificationCell, recordQualificationJudgePanel, qualificationInvocationStatus, readQualificationSpoolConfig, superviseQualificationInvocation, validateQualificationRunnerSpool, verifyQualificationExecutable, } from "@skill-harness/core";
|
|
5
|
+
function flag(args, name) {
|
|
6
|
+
const value = args.flags[name];
|
|
7
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function required(args, name) {
|
|
10
|
+
const value = flag(args, name);
|
|
11
|
+
if (!value)
|
|
12
|
+
throw new Error(`qualification ${args._[0] ?? "command"} requires --${name} <value>`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function integerFlag(args, name, fallback) {
|
|
16
|
+
const raw = flag(args, name);
|
|
17
|
+
if (raw === undefined)
|
|
18
|
+
return fallback;
|
|
19
|
+
const value = Number(raw);
|
|
20
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
21
|
+
throw new Error(`qualification --${name} must be a non-negative integer`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export function qualificationSupervisorRuntimeArgs(mode, scriptPath) {
|
|
25
|
+
// Production supervisors execute built JavaScript with no inherited Node
|
|
26
|
+
// loader/import/debug hooks. Source-mode tests need exactly the repository's
|
|
27
|
+
// tsx import and nothing from the parent process.execArgv.
|
|
28
|
+
return mode === "test" && scriptPath.endsWith(".ts") ? ["--import", "tsx"] : [];
|
|
29
|
+
}
|
|
30
|
+
export function consumeQualificationContinuationAuthority(path) {
|
|
31
|
+
let fd;
|
|
32
|
+
try {
|
|
33
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
34
|
+
const stat = fstatSync(fd);
|
|
35
|
+
if (!stat.isFile())
|
|
36
|
+
throw new Error("qualification continuation authority must be a regular non-symlink file");
|
|
37
|
+
if (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.()))
|
|
38
|
+
throw new Error("qualification continuation authority file must be mode 0600 and owned by the qualification user");
|
|
39
|
+
if (stat.size < 32 || stat.size > 4096)
|
|
40
|
+
throw new Error("qualification continuation authority must be 32..4096 bytes");
|
|
41
|
+
const authority = readFileSync(fd, "utf8");
|
|
42
|
+
const pathStat = lstatSync(path);
|
|
43
|
+
if (pathStat.isSymbolicLink() || pathStat.dev !== stat.dev || pathStat.ino !== stat.ino)
|
|
44
|
+
throw new Error("qualification continuation authority path changed while it was consumed");
|
|
45
|
+
unlinkSync(path);
|
|
46
|
+
if (!authority.trim())
|
|
47
|
+
throw new Error("qualification continuation authority must not be blank");
|
|
48
|
+
return authority;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (fd !== undefined)
|
|
52
|
+
closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function print(value) {
|
|
56
|
+
process.stdout.write(`${qualificationCanonicalJson(value)}\n`);
|
|
57
|
+
}
|
|
58
|
+
export async function cmdQualification(args) {
|
|
59
|
+
const operation = args._[0];
|
|
60
|
+
if (!operation)
|
|
61
|
+
throw new Error("usage: skill-harness qualification <prepare|start|status|poll|validate|panel|cell|abort> ...");
|
|
62
|
+
if (operation === "prepare") {
|
|
63
|
+
const invocation = prepareQualificationInvocation({
|
|
64
|
+
spool_dir: required(args, "spool"),
|
|
65
|
+
config_path: required(args, "config"),
|
|
66
|
+
request_path: required(args, "request"),
|
|
67
|
+
expected_configuration_sha256: flag(args, "expected-config-sha256"),
|
|
68
|
+
});
|
|
69
|
+
print({ invocation_id: invocation.invocation_id, phase: "prepared", consumed_calls: 0 });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const spool = required(args, "spool");
|
|
73
|
+
if (operation === "validate") {
|
|
74
|
+
print(validateQualificationRunnerSpool(spool));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (operation === "panel") {
|
|
78
|
+
print(recordQualificationJudgePanel({
|
|
79
|
+
spool_dir: spool,
|
|
80
|
+
panel_id: required(args, "panel-id"),
|
|
81
|
+
member_invocation_ids: required(args, "members").split(",").filter(Boolean),
|
|
82
|
+
}));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (operation === "cell") {
|
|
86
|
+
print(recordQualificationCell({
|
|
87
|
+
spool_dir: spool,
|
|
88
|
+
cell_id: required(args, "cell-id"),
|
|
89
|
+
}));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const id = required(args, "id");
|
|
93
|
+
if (operation === "status") {
|
|
94
|
+
print(qualificationInvocationStatus(spool, id));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (operation === "poll") {
|
|
98
|
+
print(await pollQualificationInvocation({
|
|
99
|
+
spool_dir: spool,
|
|
100
|
+
invocation_id: id,
|
|
101
|
+
wait_ms: integerFlag(args, "wait-ms", 0),
|
|
102
|
+
interval_ms: Math.max(5, integerFlag(args, "interval-ms", 100)),
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (operation === "abort") {
|
|
107
|
+
print(await abortQualificationInvocation({
|
|
108
|
+
spool_dir: spool,
|
|
109
|
+
invocation_id: id,
|
|
110
|
+
reason: required(args, "reason"),
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (operation === "__supervise") {
|
|
115
|
+
const fdRaw = flag(args, "authority-fd");
|
|
116
|
+
if (fdRaw !== "3")
|
|
117
|
+
throw new Error("qualification internal authority descriptor must be fd 3");
|
|
118
|
+
let authority;
|
|
119
|
+
try {
|
|
120
|
+
authority = JSON.parse(readFileSync(3, "utf8"));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error("qualification internal authority payload is invalid JSON");
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
closeSync(3);
|
|
127
|
+
}
|
|
128
|
+
if (!authority || typeof authority !== "object" || Array.isArray(authority) ||
|
|
129
|
+
Object.keys(authority).sort().join(",") !== "authentication_authority,continuation_authority") {
|
|
130
|
+
throw new Error("qualification internal authority payload is not closed");
|
|
131
|
+
}
|
|
132
|
+
const payload = authority;
|
|
133
|
+
if (typeof payload.authentication_authority !== "string" ||
|
|
134
|
+
(payload.continuation_authority !== null && typeof payload.continuation_authority !== "string")) {
|
|
135
|
+
throw new Error("qualification internal authority payload has invalid fields");
|
|
136
|
+
}
|
|
137
|
+
await superviseQualificationInvocation({
|
|
138
|
+
spool_dir: spool,
|
|
139
|
+
invocation_id: id,
|
|
140
|
+
child_env: process.env,
|
|
141
|
+
authentication_authority: payload.authentication_authority,
|
|
142
|
+
continuation_authority: payload.continuation_authority ?? undefined,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (operation === "start") {
|
|
147
|
+
const config = readQualificationSpoolConfig(spool);
|
|
148
|
+
const before = qualificationInvocationStatus(spool, id);
|
|
149
|
+
if (before.phase === "terminal" || ((before.phase === "running" || before.phase === "launch-claimed") && before.supervisor_alive === true)) {
|
|
150
|
+
print(before);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const needsContinuation = before.attempt === 1 && before.supervisor_alive !== true;
|
|
154
|
+
if (needsContinuation && !flag(args, "continuation-authority-file")) {
|
|
155
|
+
throw new Error("qualification interrupted claimed invocation requires --continuation-authority-file");
|
|
156
|
+
}
|
|
157
|
+
if (config.mode === "production") {
|
|
158
|
+
const pinned = verifyQualificationExecutable(config.runner.executable);
|
|
159
|
+
const running = realpathSync(process.argv[1]);
|
|
160
|
+
if (running !== pinned.realpath) {
|
|
161
|
+
throw new Error(`qualification runner executable mismatch: configuration pins ${pinned.realpath}, running ${running}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const auth = await checkQualificationAuthentication({ spool_dir: spool, invocation_id: id });
|
|
165
|
+
if (flag(args, "continuation-authority") !== undefined)
|
|
166
|
+
throw new Error("qualification continuation authority must use --continuation-authority-file, never argv text");
|
|
167
|
+
const continuationFile = flag(args, "continuation-authority-file");
|
|
168
|
+
const continuation = continuationFile ? consumeQualificationContinuationAuthority(continuationFile) : undefined;
|
|
169
|
+
const childArgs = [
|
|
170
|
+
...qualificationSupervisorRuntimeArgs(config.mode, process.argv[1]),
|
|
171
|
+
process.argv[1],
|
|
172
|
+
"qualification", "__supervise",
|
|
173
|
+
"--spool", spool,
|
|
174
|
+
"--id", id,
|
|
175
|
+
"--authority-fd", "3",
|
|
176
|
+
];
|
|
177
|
+
const supervisor = spawn(process.execPath, childArgs, {
|
|
178
|
+
cwd: process.cwd(),
|
|
179
|
+
env: auth.child_env,
|
|
180
|
+
detached: true,
|
|
181
|
+
stdio: ["ignore", "ignore", "ignore", "pipe"],
|
|
182
|
+
});
|
|
183
|
+
await new Promise((resolve, reject) => {
|
|
184
|
+
supervisor.once("spawn", resolve);
|
|
185
|
+
supervisor.once("error", reject);
|
|
186
|
+
});
|
|
187
|
+
const authorityPipe = supervisor.stdio[3];
|
|
188
|
+
if (!authorityPipe || !("write" in authorityPipe))
|
|
189
|
+
throw new Error("qualification private authority pipe was not created");
|
|
190
|
+
authorityPipe.end(JSON.stringify({ authentication_authority: auth.launch_authority, continuation_authority: continuation ?? null }));
|
|
191
|
+
supervisor.unref();
|
|
192
|
+
const deadline = Date.now() + integerFlag(args, "ack-wait-ms", 1500);
|
|
193
|
+
const priorSupervisor = qualificationCanonicalJson(before.supervisor);
|
|
194
|
+
let status = qualificationInvocationStatus(spool, id);
|
|
195
|
+
const acknowledged = () => needsContinuation
|
|
196
|
+
? status.phase === "terminal" || (status.supervisor_alive === true && qualificationCanonicalJson(status.supervisor) !== priorSupervisor)
|
|
197
|
+
: status.phase !== "prepared";
|
|
198
|
+
while (!acknowledged() && Date.now() < deadline) {
|
|
199
|
+
await sleep(10);
|
|
200
|
+
status = qualificationInvocationStatus(spool, id);
|
|
201
|
+
}
|
|
202
|
+
if (!acknowledged()) {
|
|
203
|
+
throw new Error(`qualification supervisor did not durably acknowledge ${id} before the acknowledgement deadline`);
|
|
204
|
+
}
|
|
205
|
+
print(status);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`unknown qualification operation: ${operation}`);
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=qualification.js.map
|
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, refreshRubricHashes, findJudgeRawFiles, effectiveThreshold, scoreContextFor, isScoredMode, rebuildScenarioResult, mergeScenarioMetrics, envFlag, planAdjudication, adjudicateRun, assertJudgeAllowed, cellsFromResults, } from "@skill-harness/core";
|
|
6
|
+
import { collectReport, renderReport, collectTrends, readResults, writeResults, applyOverride, preserveTranscript, findTranscriptFiles, ensureResultsGitignore, appendJournal, loadSpec, regradeScenario, appendJudgeHistory, refreshRubricHashes, findJudgeRawFiles, effectiveThreshold, scoreContextFor, isScoredMode, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, envFlag, planAdjudication, adjudicateRun, assertJudgeAllowed, cellsFromResults, } 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/. */
|
|
@@ -115,17 +115,23 @@ export async function serveReview(opts) {
|
|
|
115
115
|
res.writeHead(404).end("unknown scenario");
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
+
const prev = results.scenarios.find((s) => s.id === body.scenarioId);
|
|
119
|
+
if (!prev) {
|
|
120
|
+
res.writeHead(404).end("scenario not in this run");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const delivery = prev.objective?.assertions.find(assertion => assertion.kind === "skill_delivered");
|
|
124
|
+
if (results.schema === 3 && delivery?.status !== "PASS") {
|
|
125
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
126
|
+
res.end(JSON.stringify({ ok: false, error: `scenario ${body.scenarioId} is ${delivery?.status ?? "ERROR"}: delivery-gated evidence cannot be re-judged` }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
118
129
|
const adapter = opts.adapter ?? getAdapter(results.harness);
|
|
119
130
|
if (!(await adapter.available())) {
|
|
120
131
|
res.writeHead(400, { "content-type": "application/json" });
|
|
121
132
|
res.end(JSON.stringify({ ok: false, error: `harness \`${results.harness}\` is not on PATH` }));
|
|
122
133
|
return;
|
|
123
134
|
}
|
|
124
|
-
const prev = results.scenarios.find((s) => s.id === body.scenarioId);
|
|
125
|
-
if (!prev) {
|
|
126
|
-
res.writeHead(404).end("scenario not in this run");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
135
|
const threshold = effectiveThreshold(prev, scenario);
|
|
130
136
|
try {
|
|
131
137
|
const rr = await regradeScenario({
|
|
@@ -133,12 +139,21 @@ export async function serveReview(opts) {
|
|
|
133
139
|
specDir: dirname(specPath), threshold, mode: results.mode,
|
|
134
140
|
expectedReps: prev.reps ?? 1,
|
|
135
141
|
});
|
|
136
|
-
const merged = results.scenarios.map((s) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
const merged = results.scenarios.map((s) => {
|
|
143
|
+
if (s.id !== body.scenarioId)
|
|
144
|
+
return s;
|
|
145
|
+
const judge_history = appendJudgeHistory(s, results.judge, {
|
|
146
|
+
judge: results.judge,
|
|
147
|
+
verdict: rr.judge_verdict,
|
|
148
|
+
reason: rr.judge_reason,
|
|
149
|
+
suspect: rr.suspect,
|
|
150
|
+
criteria: rr.rep_judgments?.find(panel => panel.repetition === 0)?.judgments[0]?.criteria,
|
|
151
|
+
});
|
|
152
|
+
// Same contract as `grade`, through the same choke point.
|
|
153
|
+
return rebuildScenarioResult({ ...rr, metrics: mergeScenarioMetrics(s.metrics, rr.metrics), rep_judgments: carryRepObjectives(rr.rep_judgments, s.rep_judgments), judge_history }, s, { objective: "carry", adjudication: "drop" });
|
|
154
|
+
});
|
|
141
155
|
const written = writeResults(column.runDir, {
|
|
156
|
+
schema: results.schema, subject_invocations: results.subject_invocations,
|
|
142
157
|
skill: results.skill, harness: results.harness, model: results.model, judge: results.judge,
|
|
143
158
|
timestamp: results.timestamp, label: results.label, mode: results.mode, scenarios: merged,
|
|
144
159
|
partial: results.partial,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"node": ">=20"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"prepack": "
|
|
44
|
+
"prepack": "node ../../scripts/release-pack.mjs --guard"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@skill-harness/core": "0.
|
|
48
|
-
"@skill-harness/adapters": "0.
|
|
47
|
+
"@skill-harness/core": "0.12.0",
|
|
48
|
+
"@skill-harness/adapters": "0.12.0"
|
|
49
49
|
}
|
|
50
50
|
}
|