@skill-harness/core 0.7.0 → 0.9.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/dist/adapters/types.d.ts +8 -0
- package/dist/adjudication.d.ts +1 -0
- package/dist/adjudication.js +26 -4
- package/dist/capture-trace-types.d.ts +19 -0
- package/dist/comparison.d.ts +96 -0
- package/dist/comparison.js +183 -0
- package/dist/execution-trace.js +93 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lint.d.ts +7 -0
- package/dist/lint.js +14 -2
- package/dist/regate.d.ts +1 -1
- package/dist/regate.js +94 -21
- package/dist/regrade.d.ts +4 -0
- package/dist/regrade.js +29 -5
- package/dist/report.d.ts +7 -0
- package/dist/report.js +6 -0
- package/dist/reps.d.ts +8 -1
- package/dist/reps.js +47 -13
- package/dist/rescore.js +2 -2
- package/dist/restamp.d.ts +68 -0
- package/dist/restamp.js +194 -0
- package/dist/results.d.ts +28 -0
- package/dist/results.js +56 -4
- package/dist/run.js +96 -35
- package/dist/sandbox.d.ts +32 -0
- package/dist/sandbox.js +12 -0
- package/dist/score.d.ts +3 -1
- package/dist/score.js +20 -5
- package/dist/seeded.d.ts +7 -1
- package/dist/seeded.js +29 -8
- package/dist/sources.d.ts +50 -0
- package/dist/sources.js +186 -7
- package/dist/spec.d.ts +15 -0
- package/dist/spec.js +53 -1
- package/dist/stability.js +26 -4
- package/dist/trace-gates.d.ts +2 -1
- package/dist/trace-gates.js +4 -1
- package/dist/trajectory-gates.d.ts +137 -0
- package/dist/trajectory-gates.js +592 -0
- package/package.json +5 -3
- package/schemas/results-v2.schema.json +133 -0
- package/schemas/specification-v1.schema.json +217 -0
- package/schemas/trajectory-event-v1.schema.json +61 -0
package/dist/lint.js
CHANGED
|
@@ -4,7 +4,7 @@ import yaml from "js-yaml";
|
|
|
4
4
|
import { loadSpec, SpecError } from "./spec.js";
|
|
5
5
|
import { computeCoverage } from "./instruction-coverage.js";
|
|
6
6
|
import { readResults, finalizeResults, findTranscriptFiles, resultsPath, scoreContextFor } from "./results.js";
|
|
7
|
-
import { currentHashFor, describeSourceKey, remedyForKey, scenarioIdForKey, effectiveFixture, SCENARIO_PREFIX, STIMULUS_PREFIX, UNREADABLE } from "./sources.js";
|
|
7
|
+
import { currentHashFor, describeSourceKey, isSupersededKey, remedyForKey, scenarioIdForKey, effectiveFixture, SCENARIO_PREFIX, STIMULUS_PREFIX, UNREADABLE } from "./sources.js";
|
|
8
8
|
import { downgradeWarning } from "./downgrade.js";
|
|
9
9
|
import { collectScoredRuns } from "./trends.js";
|
|
10
10
|
import { boundaryCells, stabilityFrom, stabilityNote } from "./stability.js";
|
|
@@ -60,6 +60,12 @@ export function lintSkill(skillDir) {
|
|
|
60
60
|
findings.push({ skill, code: "consistency", message: stale.replace(/^warning: /, "") });
|
|
61
61
|
}
|
|
62
62
|
// ship_bar sanity
|
|
63
|
+
if (!Number.isInteger(spec.ship_bar.total)) {
|
|
64
|
+
findings.push({ skill, code: "ship_bar", message: "ship_bar.total must be an integer" });
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isInteger(spec.ship_bar.min_pass)) {
|
|
67
|
+
findings.push({ skill, code: "ship_bar", message: "ship_bar.min_pass must be an integer" });
|
|
68
|
+
}
|
|
63
69
|
if (spec.ship_bar.total < 1) {
|
|
64
70
|
findings.push({ skill, code: "ship_bar", message: "ship_bar.total must be >= 1" });
|
|
65
71
|
}
|
|
@@ -247,6 +253,12 @@ export function lintSkill(skillDir) {
|
|
|
247
253
|
const newest = full.runDir;
|
|
248
254
|
const ctx = { skillDir, specDir, scenarios: spec.scenarios, judgePersona: spec.judge_persona };
|
|
249
255
|
for (const [key, recorded] of Object.entries(hashes)) {
|
|
256
|
+
// A raw-bytes key whose model-visible digest this run also recorded is not the
|
|
257
|
+
// honest comparison: it reports a `description`-reword or an `allowed-tools:`
|
|
258
|
+
// line — text no graded run could observe — as a reason to re-run. The pair is
|
|
259
|
+
// recorded together and exactly one of them is compared.
|
|
260
|
+
if (isSupersededKey(key, hashes))
|
|
261
|
+
continue;
|
|
250
262
|
const what = describeSourceKey(key);
|
|
251
263
|
const scenario = scenarioIdForKey(key, spec.scenarios);
|
|
252
264
|
// The run itself failed to hash this source, so it was never verified and
|
|
@@ -349,7 +361,7 @@ function runDirsNewestFirst(tagDir) {
|
|
|
349
361
|
* TOCTOU removal between readdir and statSync) are skipped rather than propagated, so a
|
|
350
362
|
* single bad entry can't abort lintSkill's "never throws" contract.
|
|
351
363
|
*/
|
|
352
|
-
function enumerateRunDirs(resultsRoot) {
|
|
364
|
+
export function enumerateRunDirs(resultsRoot) {
|
|
353
365
|
if (!existsSync(resultsRoot))
|
|
354
366
|
return [];
|
|
355
367
|
const out = [];
|
package/dist/regate.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface RegateChange {
|
|
|
15
15
|
from: Verdict;
|
|
16
16
|
to: Verdict;
|
|
17
17
|
/** What the corrected needles say about this scenario now. */
|
|
18
|
-
gate: "pass" | "fail";
|
|
18
|
+
gate: "pass" | "fail" | "error";
|
|
19
19
|
/** Whether re-deciding it required a judge call (only a gate-FAIL → gate-pass flip does). */
|
|
20
20
|
judged: boolean;
|
|
21
21
|
}
|
package/dist/regate.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { parseVerdict, detectMisfire } from "./grade.js";
|
|
4
4
|
import { evaluateNeedleGates, hasNeedleGates } from "./seeded.js";
|
|
5
5
|
import { evaluateTraceGates } from "./trace-gates.js";
|
|
6
6
|
import { mergeTraces, deserializeTrace } from "./execution-trace.js";
|
|
7
|
+
import { deserializeTrajectoryEvents, evaluateTrajectoryGates } from "./trajectory-gates.js";
|
|
7
8
|
import { judgeOneRep } from "./regrade.js";
|
|
8
|
-
import { readResults, writeResults, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, tracePath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, } from "./results.js";
|
|
9
|
+
import { readResults, writeResults, diffPath, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, findTrajectoryFiles, tracePath, trajectoryPath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, } from "./results.js";
|
|
9
10
|
import { outcomesToResult } from "./reps.js";
|
|
10
11
|
import { appendJournal } from "./journal.js";
|
|
11
12
|
import { gatesDigest, GATES_PREFIX } from "./sources.js";
|
|
@@ -88,30 +89,39 @@ export async function regateRun(opts) {
|
|
|
88
89
|
const s = specById.get(rec.id);
|
|
89
90
|
const needles = hasNeedleGates(s ?? {});
|
|
90
91
|
const traceGated = Boolean(s?.traceAssert);
|
|
91
|
-
|
|
92
|
+
const trajectoryGated = Boolean(s?.trajectoryAssert);
|
|
93
|
+
if (!s || (!needles && !traceGated && !trajectoryGated))
|
|
92
94
|
continue; // nothing for regate to re-decide
|
|
93
95
|
if (s.assert?.vitest || s.assert?.post_test) {
|
|
94
96
|
blocked.push(`${s.id}: declares ${s.assert.vitest ? "assert.vitest" : "assert.post_test"}, which needs the workspace — ` +
|
|
95
97
|
`no saved artifact can stand in for it, so this scenario needs a re-run`);
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
const expectedReps = rec.reps ?? 1;
|
|
101
|
+
const expectedSuffixes = expectedReps === 1 ? [undefined] : Array.from({ length: expectedReps }, (_, index) => index);
|
|
102
|
+
const complete = (paths, expectedPaths) => paths.length === expectedPaths.length && expectedPaths.every((path) => paths.includes(basename(path)));
|
|
103
|
+
if (needles && !complete(findDiffFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => diffPath(opts.runDir, s.id, mode, rep)))) {
|
|
104
|
+
blocked.push(`${s.id}: staged-diff artifacts are incomplete for ${expectedReps} recorded rep(s) — regate refuses a partial repetition set`);
|
|
100
105
|
continue;
|
|
101
106
|
}
|
|
102
107
|
// A trace gate is only re-decidable from a saved trace. A run recorded before
|
|
103
108
|
// traces existed has none, and saying so is the whole point — pretending
|
|
104
109
|
// regate can answer would report a verdict derived from no evidence.
|
|
105
|
-
if (traceGated && findTraceFiles(opts.runDir, s.id, mode).
|
|
106
|
-
blocked.push(`${s.id}:
|
|
107
|
-
`(
|
|
110
|
+
if (traceGated && !complete(findTraceFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => tracePath(opts.runDir, s.id, mode, rep)))) {
|
|
111
|
+
blocked.push(`${s.id}: assert.trace artifacts are incomplete for ${expectedReps} recorded rep(s) ` +
|
|
112
|
+
`(traces are gitignored, or capture failed) — it needs a re-run`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (trajectoryGated && !complete(findTrajectoryFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => trajectoryPath(opts.runDir, s.id, mode, rep)))) {
|
|
116
|
+
blocked.push(`${s.id}: assert.trajectory artifacts are incomplete for ${expectedReps} recorded rep(s) ` +
|
|
117
|
+
`(events are gitignored, or required native evidence was missing) — it needs a re-run`);
|
|
108
118
|
continue;
|
|
109
119
|
}
|
|
110
120
|
targets.push(s);
|
|
111
121
|
}
|
|
112
122
|
if (targets.length === 0) {
|
|
113
123
|
throw new Error(`nothing to regate in ${opts.runDir}` +
|
|
114
|
-
(blocked.length > 0 ? `:\n ${blocked.join("\n ")}` : " — no scenario declares diff_contains/diff_excludes or assert.
|
|
124
|
+
(blocked.length > 0 ? `:\n ${blocked.join("\n ")}` : " — no scenario declares diff_contains/diff_excludes, assert.trace, or assert.trajectory"));
|
|
115
125
|
}
|
|
116
126
|
const changes = [];
|
|
117
127
|
let judgeCalls = 0;
|
|
@@ -124,17 +134,21 @@ export async function regateRun(opts) {
|
|
|
124
134
|
}
|
|
125
135
|
const diffFiles = findDiffFiles(opts.runDir, scenario.id, mode);
|
|
126
136
|
const traceFiles = findTraceFiles(opts.runDir, scenario.id, mode);
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
//
|
|
137
|
+
const eventFiles = findTrajectoryFiles(opts.runDir, scenario.id, mode);
|
|
138
|
+
// Reps come from whichever artifact this scenario actually has. A trajectory-only
|
|
139
|
+
// scenario has neither diff nor trace, so falling back only once would regate
|
|
140
|
+
// nothing and report success.
|
|
130
141
|
const repKeys = diffFiles.length > 0
|
|
131
142
|
? diffFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: f }))
|
|
132
|
-
: traceFiles.
|
|
143
|
+
: traceFiles.length > 0
|
|
144
|
+
? traceFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }))
|
|
145
|
+
: eventFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }));
|
|
133
146
|
const outcomes = [];
|
|
134
147
|
// Per scenario, not run-wide: with several regated scenarios, a global counter
|
|
135
148
|
// would report every change as "re-judged" because some other scenario was.
|
|
136
149
|
let judgedHere = 0;
|
|
137
150
|
let gateFailedHere = false;
|
|
151
|
+
let gateErroredHere = false;
|
|
138
152
|
for (const { rep, diffFile } of repKeys) {
|
|
139
153
|
const diff = diffFile ? readFileSync(join(opts.runDir, diffFile), "utf8") : "";
|
|
140
154
|
const needleGate = diffFile ? evaluateNeedleGates(scenario, diff) : { lines: [], failure: null };
|
|
@@ -166,14 +180,69 @@ export async function regateRun(opts) {
|
|
|
166
180
|
}
|
|
167
181
|
else {
|
|
168
182
|
const g = evaluateTraceGates(scenario.traceAssert, merged);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
const expectedHash = rec.objective?.rep_trace_sha256?.[rep ?? 0] ?? (rec.reps === undefined ? rec.objective?.trace_sha256 : undefined);
|
|
184
|
+
const digestMismatch = expectedHash !== undefined && expectedHash !== merged.trace_sha256;
|
|
185
|
+
const integrityAssertion = digestMismatch
|
|
186
|
+
? [{ kind: "trace_evidence", status: "ERROR", detail: "saved trace no longer matches the hash recorded by the run" }]
|
|
187
|
+
: [];
|
|
188
|
+
objective = { status: digestMismatch ? "ERROR" : g.status, trace_version: merged.trace_version, trace_sha256: merged.trace_sha256, assertions: [...integrityAssertion, ...g.assertions] };
|
|
189
|
+
if (g.status === "FAIL" || g.status === "ERROR" || digestMismatch) {
|
|
190
|
+
const bad = [...integrityAssertion.map((x) => x.detail), ...g.assertions.filter((x) => x.status === g.status && g.status !== "PASS").map((x) => x.detail)];
|
|
172
191
|
traceFailure = `objective: ${bad.join("; ")}`;
|
|
173
192
|
}
|
|
174
193
|
}
|
|
175
194
|
}
|
|
176
|
-
|
|
195
|
+
// Adapter-neutral trajectory gate, replayed from the saved normalized
|
|
196
|
+
// event artifact. Native ledgers are deliberately not re-read here: the
|
|
197
|
+
// artifact is the immutable evidence this run actually captured.
|
|
198
|
+
let trajectoryFailure = null;
|
|
199
|
+
if (scenario.trajectoryAssert) {
|
|
200
|
+
const ep = trajectoryPath(opts.runDir, scenario.id, mode, rep);
|
|
201
|
+
const events = existsSync(ep) ? deserializeTrajectoryEvents(readFileSync(ep, "utf8")) : null;
|
|
202
|
+
// The normalized artifact contains only events that were successfully captured.
|
|
203
|
+
// Source-read/normalization failures are persisted on the original objective;
|
|
204
|
+
// dropping them here would let a partial ledger become PASS on replay.
|
|
205
|
+
const priorEvidenceErrors = (rec.objective?.assertions ?? [])
|
|
206
|
+
.filter((result) => result.kind === "trajectory_evidence" && result.status === "ERROR");
|
|
207
|
+
if (!events) {
|
|
208
|
+
trajectoryFailure = "objective: saved normalized events are missing, malformed, or from an unsupported version";
|
|
209
|
+
objective = {
|
|
210
|
+
...(objective ?? { assertions: [] }),
|
|
211
|
+
status: "ERROR",
|
|
212
|
+
assertions: [...(objective?.assertions ?? []), { kind: "trajectory_evidence", status: "ERROR", detail: trajectoryFailure }],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const g = evaluateTrajectoryGates(scenario.trajectoryAssert, events);
|
|
217
|
+
const expectedHash = rec.objective?.rep_events_sha256?.[rep ?? 0] ?? (rec.reps === undefined ? rec.objective?.events_sha256 : undefined);
|
|
218
|
+
const digestMismatch = expectedHash !== undefined && expectedHash !== g.events_sha256;
|
|
219
|
+
const integrityAssertion = digestMismatch
|
|
220
|
+
? [{ kind: "trajectory_evidence", status: "ERROR", detail: "saved normalized events no longer match the hash recorded by the run" }]
|
|
221
|
+
: [];
|
|
222
|
+
const priorStatus = objective?.status ?? "PASS";
|
|
223
|
+
const status = priorStatus === "ERROR" || g.status === "ERROR" || priorEvidenceErrors.length || digestMismatch
|
|
224
|
+
? "ERROR"
|
|
225
|
+
: priorStatus === "FAIL" || g.status === "FAIL"
|
|
226
|
+
? "FAIL"
|
|
227
|
+
: "PASS";
|
|
228
|
+
objective = {
|
|
229
|
+
...(objective ?? {}),
|
|
230
|
+
status,
|
|
231
|
+
trajectory_version: g.event_version,
|
|
232
|
+
events_sha256: g.events_sha256,
|
|
233
|
+
assertions: [...(objective?.assertions ?? []), ...priorEvidenceErrors, ...integrityAssertion, ...g.assertions],
|
|
234
|
+
};
|
|
235
|
+
if (g.status !== "PASS" || priorEvidenceErrors.length || digestMismatch) {
|
|
236
|
+
const details = [
|
|
237
|
+
...priorEvidenceErrors.map((result) => result.detail),
|
|
238
|
+
...integrityAssertion.map((result) => result.detail),
|
|
239
|
+
...g.assertions.filter((result) => result.status === g.status && g.status !== "PASS").map((result) => result.detail),
|
|
240
|
+
];
|
|
241
|
+
trajectoryFailure = `objective: ${details.join("; ")}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const gate = { lines: needleGate.lines, failure: needleGate.failure ?? traceFailure ?? trajectoryFailure };
|
|
177
246
|
const tPath = transcriptPath(opts.runDir, scenario.id, mode, rep);
|
|
178
247
|
const before = existsSync(tPath) ? readFileSync(tPath, "utf8") : "";
|
|
179
248
|
// Two sources, because the two gate kinds record their prior state
|
|
@@ -189,8 +258,11 @@ export async function regateRun(opts) {
|
|
|
189
258
|
if (existsSync(tPath))
|
|
190
259
|
rewriteTranscript(tPath, gate.lines);
|
|
191
260
|
if (gate.failure) {
|
|
192
|
-
|
|
193
|
-
|
|
261
|
+
if (objective?.status === "ERROR")
|
|
262
|
+
gateErroredHere = true;
|
|
263
|
+
else
|
|
264
|
+
gateFailedHere = true;
|
|
265
|
+
outcomes.push({ verdict: objective?.status === "ERROR" ? "ERROR" : "FAIL", reason: gate.failure, suspect: false, objective });
|
|
194
266
|
continue;
|
|
195
267
|
}
|
|
196
268
|
if (!oldGateFailed) {
|
|
@@ -205,13 +277,14 @@ export async function regateRun(opts) {
|
|
|
205
277
|
outcomes.push({ ...(await judgeOneRep({
|
|
206
278
|
runDir: opts.runDir, spec: opts.spec, scenario, transcript,
|
|
207
279
|
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir,
|
|
208
|
-
mode, rep, now,
|
|
280
|
+
mode, rep, now, rejudge: true,
|
|
209
281
|
})), objective });
|
|
210
282
|
judgeCalls++;
|
|
211
283
|
judgedHere++;
|
|
212
284
|
}
|
|
213
285
|
const threshold = effectiveThreshold(rec, scenario);
|
|
214
286
|
const next = outcomesToResult(scenario.id, outcomes, outcomes.length, threshold);
|
|
287
|
+
next.metrics = mergeScenarioMetrics(rec.metrics, next.metrics);
|
|
215
288
|
// Overrides and their notes survive: a regate re-decides the gate, and an author
|
|
216
289
|
// override is a statement about the judge, not about the needle.
|
|
217
290
|
// `regate` re-evaluates gates from saved artifacts and asks no judge anything:
|
|
@@ -222,7 +295,7 @@ export async function regateRun(opts) {
|
|
|
222
295
|
if (to !== rec.judge_verdict) {
|
|
223
296
|
changes.push({
|
|
224
297
|
id: scenario.id, from: rec.judge_verdict, to,
|
|
225
|
-
gate: gateFailedHere ? "fail" : "pass",
|
|
298
|
+
gate: gateErroredHere ? "error" : gateFailedHere ? "fail" : "pass",
|
|
226
299
|
judged: judgedHere > 0,
|
|
227
300
|
});
|
|
228
301
|
}
|
package/dist/regrade.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface RegradeOptions {
|
|
|
32
32
|
* with "nothing to re-grade", which is how ten scorable runs stayed ungraded.
|
|
33
33
|
*/
|
|
34
34
|
mode?: string;
|
|
35
|
+
/** Recorded repetition count; when supplied, missing/extra rep artifacts fail before judge calls. */
|
|
36
|
+
expectedReps?: number;
|
|
35
37
|
now?: () => string;
|
|
36
38
|
}
|
|
37
39
|
/** Judge one saved transcript: writes the judge-raw artifact, emits a `judge-verdict` journal event (plus `misfire-flag` when the verdict is suspect), and returns the outcome. */
|
|
@@ -46,6 +48,8 @@ export declare function judgeOneRep(opts: {
|
|
|
46
48
|
mode: string;
|
|
47
49
|
rep: number | undefined;
|
|
48
50
|
now: () => string;
|
|
51
|
+
/** True when this call revisits a saved subject transcript. */
|
|
52
|
+
rejudge?: boolean;
|
|
49
53
|
}): Promise<RepOutcome>;
|
|
50
54
|
/**
|
|
51
55
|
* Re-judge a scenario's saved transcript(s) for the run's mode with `judge` — no
|
package/dist/regrade.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { buildJudgePrompt, judgeInWorkspace } from "./grade.js";
|
|
4
|
-
import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, scoreContextFor, rebuildScenarioResult, } from "./results.js";
|
|
4
|
+
import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, } from "./results.js";
|
|
5
5
|
import { outcomesToResult } from "./reps.js";
|
|
6
6
|
import { appendJournal } from "./journal.js";
|
|
7
7
|
import { rubricDigest, personaDigest, RUBRIC_PREFIX, PERSONA_KEY } from "./sources.js";
|
|
@@ -36,6 +36,7 @@ export function refreshRubricHashes(recorded, spec, judgedIds) {
|
|
|
36
36
|
/** Judge one saved transcript: writes the judge-raw artifact, emits a `judge-verdict` journal event (plus `misfire-flag` when the verdict is suspect), and returns the outcome. */
|
|
37
37
|
export async function judgeOneRep(opts) {
|
|
38
38
|
const { runDir, spec, scenario, transcript, adapter, judge, specDir, mode, rep, now } = opts;
|
|
39
|
+
const startedAt = performance.now();
|
|
39
40
|
const prompt = buildJudgePrompt({ skill: spec.skill, persona: spec.judge_persona, scenario, transcript });
|
|
40
41
|
const g = await judgeInWorkspace(adapter, judge, prompt, specDir);
|
|
41
42
|
writeFileSync(judgeRawPath(runDir, scenario.id, mode, rep), g.raw, "utf8");
|
|
@@ -43,7 +44,14 @@ export async function judgeOneRep(opts) {
|
|
|
43
44
|
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict: g.verdict, reason: g.reason, suspect: g.suspect, ...repField });
|
|
44
45
|
if (g.suspect)
|
|
45
46
|
appendJournal(runDir, { event: "misfire-flag", ts: now(), id: scenario.id, reason: g.reason, ...repField });
|
|
46
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
verdict: g.verdict, reason: g.reason, suspect: g.suspect,
|
|
49
|
+
metrics: {
|
|
50
|
+
wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
51
|
+
judge_calls: 1,
|
|
52
|
+
judge_rejudge_calls: opts.rejudge ? 1 : 0,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* Re-judge a scenario's saved transcript(s) for the run's mode with `judge` — no
|
|
@@ -58,14 +66,20 @@ export async function regradeScenario(opts) {
|
|
|
58
66
|
const files = findTranscriptFiles(opts.runDir, opts.scenario.id, mode);
|
|
59
67
|
if (files.length === 0)
|
|
60
68
|
throw new Error(`no ${mode} transcripts for ${opts.scenario.id} in ${opts.runDir}`);
|
|
61
|
-
const
|
|
69
|
+
const expected = opts.expectedReps ?? files.length;
|
|
70
|
+
const expectedIndices = expected === 1 ? [null] : Array.from({ length: expected }, (_, index) => index);
|
|
71
|
+
const actualIndices = files.map((file) => repIndexOf(file)).sort((a, b) => (a ?? -1) - (b ?? -1));
|
|
72
|
+
if (files.length !== expected || JSON.stringify(actualIndices) !== JSON.stringify(expectedIndices)) {
|
|
73
|
+
throw new Error(`${opts.scenario.id}: transcript artifacts are incomplete for ${expected} recorded rep(s) — re-run instead of grading a smaller repetition set`);
|
|
74
|
+
}
|
|
75
|
+
const repCount = expected;
|
|
62
76
|
const outcomes = [];
|
|
63
77
|
for (const file of files) {
|
|
64
78
|
const rep = repIndexOf(file) ?? undefined;
|
|
65
79
|
const transcript = readFileSync(join(opts.runDir, file), "utf8");
|
|
66
80
|
outcomes.push(await judgeOneRep({
|
|
67
81
|
runDir: opts.runDir, spec: opts.spec, scenario: opts.scenario, transcript,
|
|
68
|
-
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir, mode, rep, now,
|
|
82
|
+
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir, mode, rep, now, rejudge: true,
|
|
69
83
|
}));
|
|
70
84
|
}
|
|
71
85
|
return outcomesToResult(opts.scenario.id, outcomes, repCount, opts.threshold);
|
|
@@ -120,7 +134,15 @@ export async function regradeRun(opts) {
|
|
|
120
134
|
return prev;
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
|
-
const
|
|
137
|
+
const completeTranscripts = (record) => {
|
|
138
|
+
const expected = record.reps ?? 1;
|
|
139
|
+
const files = findTranscriptFiles(runDir, record.id, mode);
|
|
140
|
+
const expectedIndices = expected === 1 ? [null] : Array.from({ length: expected }, (_, index) => index);
|
|
141
|
+
const actualIndices = files.map((file) => repIndexOf(file)).sort((a, b) => (a ?? -1) - (b ?? -1));
|
|
142
|
+
return files.length === expected && JSON.stringify(actualIndices) === JSON.stringify(expectedIndices);
|
|
143
|
+
};
|
|
144
|
+
const recordedById = new Map(recorded.map((record) => [record.id, record]));
|
|
145
|
+
const missing = targets.filter((id) => !specById.has(id) || !completeTranscripts(recordedById.get(id)));
|
|
124
146
|
if (missing.length === targets.length) {
|
|
125
147
|
throw new Error(`no ${mode} transcripts in ${runDir} — nothing to re-grade`);
|
|
126
148
|
}
|
|
@@ -140,8 +162,10 @@ export async function regradeRun(opts) {
|
|
|
140
162
|
const threshold = effectiveThreshold(prevScenario, scenario);
|
|
141
163
|
const rr = await regradeScenario({
|
|
142
164
|
runDir, spec, scenario, adapter, judge, specDir, threshold, mode, now,
|
|
165
|
+
expectedReps: prevScenario?.reps ?? 1,
|
|
143
166
|
});
|
|
144
167
|
const carry = overrides.get(id);
|
|
168
|
+
rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
|
|
145
169
|
// `grade` re-judges the saved transcript. It does not re-evaluate trace gates
|
|
146
170
|
// (that is `regate`), so `objective` still describes this run; and it replaced
|
|
147
171
|
// the judgments a prior adjudication described, so that panel must go.
|
package/dist/report.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ShipBar } from "./spec.js";
|
|
2
2
|
import { type ResultsFile } from "./results.js";
|
|
3
3
|
import { type Lift } from "./lift.js";
|
|
4
|
+
import { type AggregateMetrics } from "./comparison.js";
|
|
4
5
|
export interface RunColumn {
|
|
5
6
|
index: number;
|
|
6
7
|
label: string;
|
|
@@ -8,8 +9,10 @@ export interface RunColumn {
|
|
|
8
9
|
runDir: string;
|
|
9
10
|
timestamp: string;
|
|
10
11
|
mode: string;
|
|
12
|
+
partial: boolean;
|
|
11
13
|
grade: ResultsFile["effective_grade"];
|
|
12
14
|
judge: ResultsFile["judge"];
|
|
15
|
+
metrics: AggregateMetrics;
|
|
13
16
|
cells: Record<string, {
|
|
14
17
|
judge_verdict: string;
|
|
15
18
|
judge_reason: string;
|
|
@@ -30,6 +33,7 @@ export interface RunColumn {
|
|
|
30
33
|
passes?: number;
|
|
31
34
|
clean?: number;
|
|
32
35
|
flakiness?: number;
|
|
36
|
+
metrics?: import("./results.js").ScenarioMetrics;
|
|
33
37
|
override: string | null;
|
|
34
38
|
note: string;
|
|
35
39
|
/**
|
|
@@ -92,11 +96,13 @@ export declare function publicView(data: ReportData): {
|
|
|
92
96
|
tag: string;
|
|
93
97
|
timestamp: string;
|
|
94
98
|
mode: string;
|
|
99
|
+
partial: boolean;
|
|
95
100
|
grade: import("./results.js").GradeSummary;
|
|
96
101
|
judge: {
|
|
97
102
|
provider: string;
|
|
98
103
|
model: string;
|
|
99
104
|
};
|
|
105
|
+
metrics: AggregateMetrics;
|
|
100
106
|
cells: Record<string, {
|
|
101
107
|
judge_verdict: string;
|
|
102
108
|
judge_reason: string;
|
|
@@ -117,6 +123,7 @@ export declare function publicView(data: ReportData): {
|
|
|
117
123
|
passes?: number;
|
|
118
124
|
clean?: number;
|
|
119
125
|
flakiness?: number;
|
|
126
|
+
metrics?: import("./results.js").ScenarioMetrics;
|
|
120
127
|
override: string | null;
|
|
121
128
|
note: string;
|
|
122
129
|
/**
|
package/dist/report.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadSpec } from "./spec.js";
|
|
|
4
4
|
import { readResults } from "./results.js";
|
|
5
5
|
import { collectLift, liftHeadline } from "./lift.js";
|
|
6
6
|
import { boundaryCells, collectStability, stabilityNote } from "./stability.js";
|
|
7
|
+
import { aggregateMetrics } from "./comparison.js";
|
|
7
8
|
/** Most-recent run dir (by name, which is an ISO-ish slug) under a model-tag dir. */
|
|
8
9
|
function latestRunDir(tagDir) {
|
|
9
10
|
if (!statSync(tagDir).isDirectory())
|
|
@@ -83,6 +84,7 @@ export function collectReport(skillDir) {
|
|
|
83
84
|
passes: s.passes,
|
|
84
85
|
clean: s.clean,
|
|
85
86
|
flakiness: s.flakiness,
|
|
87
|
+
metrics: s.metrics,
|
|
86
88
|
override: s.override,
|
|
87
89
|
note: s.note,
|
|
88
90
|
};
|
|
@@ -105,8 +107,10 @@ export function collectReport(skillDir) {
|
|
|
105
107
|
runDir,
|
|
106
108
|
timestamp: r.timestamp,
|
|
107
109
|
mode: r.mode,
|
|
110
|
+
partial: r.partial === true,
|
|
108
111
|
grade: r.effective_grade,
|
|
109
112
|
judge: r.judge,
|
|
113
|
+
metrics: aggregateMetrics(r.scenarios),
|
|
110
114
|
cells,
|
|
111
115
|
...(lift ? { lift, liftHeadline: liftHeadline(lift) } : {}),
|
|
112
116
|
});
|
|
@@ -127,8 +131,10 @@ export function publicView(data) {
|
|
|
127
131
|
tag: c.tag,
|
|
128
132
|
timestamp: c.timestamp,
|
|
129
133
|
mode: c.mode,
|
|
134
|
+
partial: c.partial,
|
|
130
135
|
grade: c.grade,
|
|
131
136
|
judge: c.judge,
|
|
137
|
+
metrics: c.metrics,
|
|
132
138
|
cells: c.cells,
|
|
133
139
|
...(c.lift ? { lift: c.lift, liftHeadline: c.liftHeadline } : {}),
|
|
134
140
|
})),
|
package/dist/reps.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { Verdict } from "./score.js";
|
|
2
2
|
import type { ScenarioResult, ObjectiveResult } from "./results.js";
|
|
3
|
+
import type { TraceMetrics } from "./capture-trace-types.js";
|
|
3
4
|
/** One rep's outcome (subject run + judge). */
|
|
4
5
|
export interface RepOutcome {
|
|
5
6
|
verdict: Verdict;
|
|
6
7
|
reason: string;
|
|
7
8
|
suspect: boolean;
|
|
8
|
-
/** Present only when the scenario declared
|
|
9
|
+
/** Present only when the scenario declared structured objective assertions. */
|
|
9
10
|
objective?: ObjectiveResult;
|
|
11
|
+
metrics?: {
|
|
12
|
+
wall_time_ms: number;
|
|
13
|
+
judge_calls: number;
|
|
14
|
+
judge_rejudge_calls: number;
|
|
15
|
+
subject?: TraceMetrics;
|
|
16
|
+
};
|
|
10
17
|
}
|
|
11
18
|
/**
|
|
12
19
|
* Collapse per-rep objective results.
|
package/dist/reps.js
CHANGED
|
@@ -12,13 +12,16 @@ export function aggregateObjective(outcomes) {
|
|
|
12
12
|
const present = outcomes.map((o) => o.objective).filter((o) => o !== undefined);
|
|
13
13
|
if (present.length === 0)
|
|
14
14
|
return undefined;
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const picked = present.find((o) => o.status === "ERROR") ?? present.find((o) => o.status === "FAIL") ?? present[0];
|
|
16
|
+
if (present.length === 1)
|
|
17
|
+
return picked;
|
|
18
|
+
const eventHashes = present.map((objective) => objective.events_sha256);
|
|
19
|
+
const traceHashes = present.map((objective) => objective.trace_sha256);
|
|
20
|
+
return {
|
|
21
|
+
...picked,
|
|
22
|
+
...(eventHashes.every((hash) => typeof hash === "string") ? { rep_events_sha256: eventHashes } : {}),
|
|
23
|
+
...(traceHashes.every((hash) => typeof hash === "string") ? { rep_trace_sha256: traceHashes } : {}),
|
|
24
|
+
};
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
27
|
* Collapse N rep outcomes into one scenario verdict. A rep is "clean" when its
|
|
@@ -31,14 +34,17 @@ export function aggregateReps(outcomes, threshold) {
|
|
|
31
34
|
const reps = outcomes.length;
|
|
32
35
|
const clean = outcomes.filter((o) => !o.suspect);
|
|
33
36
|
const passes = clean.filter((o) => o.verdict === "PASS").length;
|
|
37
|
+
const errored = outcomes.filter((o) => o.verdict === "ERROR").length;
|
|
38
|
+
// Infrastructure/judge/tooling errors are not behavioral failures, and they
|
|
39
|
+
// cannot be voted into a pass by successful siblings. Preserve the third
|
|
40
|
+
// state so a release blocks for missing evidence without blaming the skill.
|
|
41
|
+
if (errored > 0) {
|
|
42
|
+
return { verdict: "ERROR", reason: `${errored}/${reps} reps errored — infrastructure, not behavior`, passes, reps, clean: clean.length, flakiness: 0, suspect: false };
|
|
43
|
+
}
|
|
34
44
|
if (clean.length * 2 < reps) {
|
|
35
45
|
// majority of reps misfired → untrustworthy
|
|
36
46
|
return { verdict: "FAIL", reason: `${reps - clean.length}/${reps} reps misfired — re-judge`, passes, reps, clean: clean.length, flakiness: 0, suspect: true };
|
|
37
47
|
}
|
|
38
|
-
const errored = clean.filter((o) => o.verdict === "ERROR").length;
|
|
39
|
-
if (clean.length > 0 && errored === clean.length) {
|
|
40
|
-
return { verdict: "ERROR", reason: `${errored}/${reps} reps errored`, passes: 0, reps, clean: clean.length, flakiness: 0, suspect: false };
|
|
41
|
-
}
|
|
42
48
|
const passRate = passes / clean.length;
|
|
43
49
|
const verdict = passRate >= threshold ? "PASS" : "FAIL";
|
|
44
50
|
const flakiness = 1 - Math.abs(2 * passRate - 1);
|
|
@@ -57,15 +63,43 @@ export function outcomesToResult(id, outcomes, repCount, threshold) {
|
|
|
57
63
|
// produce a result byte-identical to one from before this field existed.
|
|
58
64
|
const objective = aggregateObjective(outcomes);
|
|
59
65
|
const objectiveField = objective ? { objective } : {};
|
|
66
|
+
const metrics = aggregateMetrics(outcomes);
|
|
67
|
+
const metricsField = metrics ? { metrics } : {};
|
|
60
68
|
if (repCount === 1) {
|
|
61
69
|
const o = outcomes[0];
|
|
62
|
-
return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, override: null, note: "", ...objectiveField };
|
|
70
|
+
return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, ...metricsField, override: null, note: "", ...objectiveField };
|
|
63
71
|
}
|
|
64
72
|
const agg = aggregateReps(outcomes, threshold);
|
|
65
73
|
return {
|
|
66
74
|
id, judge_verdict: agg.verdict, judge_reason: agg.reason, suspect: agg.suspect,
|
|
67
75
|
reps: agg.reps, passes: agg.passes, clean: agg.clean, flakiness: agg.flakiness,
|
|
68
|
-
pass_threshold: threshold, override: null, note: "", ...objectiveField,
|
|
76
|
+
pass_threshold: threshold, ...metricsField, override: null, note: "", ...objectiveField,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function aggregateMetrics(outcomes) {
|
|
80
|
+
const present = outcomes.map((outcome) => outcome.metrics).filter((metrics) => metrics !== undefined);
|
|
81
|
+
if (present.length === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
const subjects = present.map((metrics) => metrics.subject).filter((metrics) => metrics !== undefined);
|
|
84
|
+
const base = {
|
|
85
|
+
wall_time_ms: present.reduce((sum, metrics) => sum + metrics.wall_time_ms, 0),
|
|
86
|
+
judge_calls: present.reduce((sum, metrics) => sum + metrics.judge_calls, 0),
|
|
87
|
+
judge_rejudge_calls: present.reduce((sum, metrics) => sum + metrics.judge_rejudge_calls, 0),
|
|
88
|
+
subject_metrics_reps: subjects.length,
|
|
89
|
+
total_reps: outcomes.length,
|
|
90
|
+
};
|
|
91
|
+
if (subjects.length === 0)
|
|
92
|
+
return base;
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
input_tokens: subjects.reduce((sum, metrics) => sum + metrics.input_tokens, 0),
|
|
96
|
+
output_tokens: subjects.reduce((sum, metrics) => sum + metrics.output_tokens, 0),
|
|
97
|
+
cache_read_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_read_tokens, 0),
|
|
98
|
+
cache_write_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_write_tokens, 0),
|
|
99
|
+
subject_cost_usd: subjects.reduce((sum, metrics) => sum + metrics.cost_usd, 0),
|
|
100
|
+
tool_calls: subjects.reduce((sum, metrics) => sum + metrics.tool_calls, 0),
|
|
101
|
+
delegated_children: subjects.reduce((sum, metrics) => sum + metrics.delegated_children, 0),
|
|
102
|
+
max_concurrency: Math.max(...subjects.map((metrics) => metrics.max_concurrency)),
|
|
69
103
|
};
|
|
70
104
|
}
|
|
71
105
|
//# sourceMappingURL=reps.js.map
|
package/dist/rescore.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readResults, writeResults, scoreContextFor, rebuildScenarioResult } from "./results.js";
|
|
3
|
+
import { readResults, writeResults, scoreContextFor, rebuildScenarioResult, effectiveThreshold } from "./results.js";
|
|
4
4
|
import { appendJournal } from "./journal.js";
|
|
5
5
|
import { policyDigest, POLICY_PREFIX } from "./sources.js";
|
|
6
6
|
/**
|
|
@@ -56,7 +56,7 @@ export function rescoreRun(opts) {
|
|
|
56
56
|
return s;
|
|
57
57
|
if (s.clean === 0)
|
|
58
58
|
return s;
|
|
59
|
-
const toThreshold = scenario
|
|
59
|
+
const toThreshold = effectiveThreshold(undefined, scenario);
|
|
60
60
|
const fromThreshold = s.pass_threshold ?? toThreshold;
|
|
61
61
|
if (toThreshold === fromThreshold)
|
|
62
62
|
return s;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade already-committed runs to the model-visible digests, without re-running anything.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a command and not a rule inside lint
|
|
5
|
+
*
|
|
6
|
+
* A stored hash is one-way. A run recorded before 0.8.0 carries only sha256 of SKILL.md's
|
|
7
|
+
* RAW BYTES, so once that file has been edited there is nothing in the record — and
|
|
8
|
+
* nothing saved beside it; run dirs hold transcripts, not a copy of the skill — that can
|
|
9
|
+
* say whether the edit touched the body or only the frontmatter. Lint therefore cannot
|
|
10
|
+
* retroactively forgive a frontmatter-only edit on a legacy record, and pretending
|
|
11
|
+
* otherwise would mean dropping the check that catches real body edits across the whole
|
|
12
|
+
* published corpus.
|
|
13
|
+
*
|
|
14
|
+
* What CAN be established, and is the entire proof this command rests on: when the
|
|
15
|
+
* recorded raw-bytes hash still equals the file's current raw-bytes hash, that run
|
|
16
|
+
* measured the exact bytes on disk right now. The model-visible digest of those same
|
|
17
|
+
* bytes is then a true statement about what that run measured, and recording it is
|
|
18
|
+
* bookkeeping rather than a claim. Any record that fails the check is left untouched —
|
|
19
|
+
* it stays honestly stale, and `run` is the only thing that can clear it.
|
|
20
|
+
*
|
|
21
|
+
* So the migration is: upgrade while the board is green, and every later
|
|
22
|
+
* frontmatter-only edit is free. Skipping it costs nothing either — a legacy record with
|
|
23
|
+
* an unedited skill keeps matching on the raw-bytes key exactly as before.
|
|
24
|
+
*
|
|
25
|
+
* ## `--from <ref>`, for when the edit already landed
|
|
26
|
+
*
|
|
27
|
+
* Nobody discovers a gate over-fires until it has over-fired, so the green worktree the
|
|
28
|
+
* paragraph above assumes is usually already gone. The pre-edit bytes are still in git,
|
|
29
|
+
* and they carry the same proof in two steps: the recorded hash matches `<ref>`'s bytes
|
|
30
|
+
* (so the run measured THOSE), and `<ref>`'s model-visible digest equals today's (so the
|
|
31
|
+
* text the model receives has not moved since). Only then is the digest recorded.
|
|
32
|
+
*
|
|
33
|
+
* It is the same proof, not a weaker one — with no `--from`, "the ref" is just the
|
|
34
|
+
* working tree, which is why both go through one code path.
|
|
35
|
+
*
|
|
36
|
+
* Free and offline: no model, no judge, no network. It rewrites only `source_hashes`.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Three exclusive buckets that SUM to `runs`, plus `partial` as a subset of `upgraded`.
|
|
40
|
+
*
|
|
41
|
+
* Summing is the point. These counters are the only thing an operator can judge a
|
|
42
|
+
* migration by, and "18 upgraded, 20 left alone" out of 140 leaves 102 records
|
|
43
|
+
* unaccounted for — which reads like the command quietly skipped them rather than like
|
|
44
|
+
* the 102 having nothing to upgrade.
|
|
45
|
+
*/
|
|
46
|
+
export interface RestampReport {
|
|
47
|
+
/** Run records examined. `upgraded + unprovable + unchanged`. */
|
|
48
|
+
runs: number;
|
|
49
|
+
/** Records that gained at least one model-visible digest, and were rewritten. */
|
|
50
|
+
upgraded: number;
|
|
51
|
+
/**
|
|
52
|
+
* Records that gained NOTHING because a document they measured has already moved —
|
|
53
|
+
* honestly stale, and `run` is the only remedy.
|
|
54
|
+
*/
|
|
55
|
+
unprovable: number;
|
|
56
|
+
/** Records with nothing to do: no `source_hashes`, unreadable, or already upgraded. */
|
|
57
|
+
unchanged: number;
|
|
58
|
+
/** Of `upgraded`, those still carrying a document that could not be proven. */
|
|
59
|
+
partial: number;
|
|
60
|
+
/** `<runDir>: <key>` for each digest added, for the CLI to print. */
|
|
61
|
+
added: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface RestampOptions {
|
|
64
|
+
/** Git ref holding the bytes the runs measured, for a tree that has already been edited. */
|
|
65
|
+
from?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Upgrade every provable record under one skill's tests/results. */
|
|
68
|
+
export declare function restampSkill(skillDir: string, opts?: RestampOptions): RestampReport;
|