@skill-harness/core 0.29.0 → 0.31.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 +16 -2
- package/dist/adapters/types.d.ts +2 -0
- package/dist/authoring-preflight.d.ts +22 -0
- package/dist/authoring-preflight.js +111 -0
- package/dist/authoring.d.ts +96 -0
- package/dist/authoring.js +451 -0
- package/dist/calibration.d.ts +138 -0
- package/dist/calibration.js +352 -0
- package/dist/canary.d.ts +28 -26
- package/dist/canary.js +109 -92
- package/dist/command-cost.d.ts +1 -1
- package/dist/command-cost.js +2 -0
- package/dist/diagnose.d.ts +17 -0
- package/dist/diagnose.js +70 -0
- package/dist/execution-trace.d.ts +2 -1
- package/dist/execution-trace.js +4 -3
- package/dist/grade.d.ts +8 -0
- package/dist/grade.js +37 -8
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/journal.d.ts +3 -0
- package/dist/judge-evidence.d.ts +40 -0
- package/dist/judge-evidence.js +120 -0
- package/dist/judge-references.d.ts +8 -0
- package/dist/judge-references.js +34 -0
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +22 -0
- package/dist/regate.js +21 -3
- package/dist/regrade.d.ts +10 -1
- package/dist/regrade.js +146 -75
- package/dist/rescore.js +1 -0
- package/dist/resource-monitor.d.ts +3 -0
- package/dist/resource-monitor.js +72 -0
- package/dist/results.d.ts +16 -4
- package/dist/results.js +10 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +147 -116
- package/dist/saved-run.d.ts +15 -0
- package/dist/saved-run.js +114 -0
- package/dist/scheduler.d.ts +3 -5
- package/dist/scheduler.js +18 -6
- package/dist/seeded.js +1 -0
- package/dist/sources.d.ts +4 -1
- package/dist/sources.js +44 -40
- package/dist/spec.d.ts +8 -0
- package/dist/spec.js +27 -0
- package/dist/trace-gates.d.ts +2 -0
- package/dist/trace-gates.js +13 -3
- package/dist/util/exec.d.ts +5 -0
- package/dist/util/exec.js +16 -32
- package/dist/util/fixture-inventory.d.ts +19 -0
- package/dist/util/fixture-inventory.js +68 -0
- package/dist/util/process.d.ts +78 -0
- package/dist/util/process.js +394 -0
- package/dist/workspace.js +13 -29
- package/package.json +1 -1
- package/schemas/results-v2.schema.json +10 -0
- package/schemas/results-v3.schema.json +10 -0
- package/schemas/specification-v1.schema.json +21 -0
package/dist/regrade.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { parseModelRef } from "./adapters/types.js";
|
|
5
|
+
import { prepareJudgePrompt, resolveJudgeReferences, formatJudgeRawReplies, judgeInWorkspace } from "./grade.js";
|
|
6
|
+
import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, tracePath, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, completeCriterionVotes, } from "./results.js";
|
|
5
7
|
import { outcomesToResult, normalizeRepOutcome } from "./reps.js";
|
|
6
8
|
import { appendJournal } from "./journal.js";
|
|
7
9
|
import { rubricDigest, personaDigest, RUBRIC_PREFIX, PERSONA_KEY } from "./sources.js";
|
|
8
10
|
import { providerFailureFromTranscript, executionFailureFromTranscript } from "./provider-failure.js";
|
|
11
|
+
import { loadExecutionFacts, transcriptResponses } from "./judge-evidence.js";
|
|
12
|
+
import { withResourceMonitoring } from "./resource-monitor.js";
|
|
13
|
+
import { throwIfProcessAborted } from "./util/process.js";
|
|
9
14
|
/**
|
|
10
15
|
* Carry a run's recorded hashes forward, refreshing only the `rubric:` keys this
|
|
11
16
|
* re-grade actually judged under (plus the persona, which applies to all of them).
|
|
@@ -18,7 +23,7 @@ import { providerFailureFromTranscript, executionFailureFromTranscript } from ".
|
|
|
18
23
|
* No hashes recorded (a pre-`source_hashes` run) stays that way: inventing hashes for
|
|
19
24
|
* a run that never recorded any would claim a coverage it does not have.
|
|
20
25
|
*/
|
|
21
|
-
export function refreshRubricHashes(recorded, spec, judgedIds) {
|
|
26
|
+
export function refreshRubricHashes(recorded, spec, judgedIds, specDir, references, refreshPersona = true) {
|
|
22
27
|
if (!recorded)
|
|
23
28
|
return undefined;
|
|
24
29
|
const next = { ...recorded };
|
|
@@ -28,12 +33,27 @@ export function refreshRubricHashes(recorded, spec, judgedIds) {
|
|
|
28
33
|
// Only refresh a key the run already carried: adding one for a scenario whose
|
|
29
34
|
// rubric was never hashed would fabricate coverage.
|
|
30
35
|
if (s && RUBRIC_PREFIX + id in next)
|
|
31
|
-
next[RUBRIC_PREFIX + id] = rubricDigest(s);
|
|
36
|
+
next[RUBRIC_PREFIX + id] = rubricDigest(s, specDir, references?.[id]);
|
|
32
37
|
}
|
|
33
|
-
if (PERSONA_KEY in next)
|
|
38
|
+
if (refreshPersona && PERSONA_KEY in next)
|
|
34
39
|
next[PERSONA_KEY] = personaDigest(spec.judge_persona);
|
|
35
40
|
return next;
|
|
36
41
|
}
|
|
42
|
+
function savedExecution(opts, mode, rep, transcript) {
|
|
43
|
+
const repetition = rep ?? 0;
|
|
44
|
+
const panel = opts.prior?.rep_judgments?.find(panel => panel.repetition === repetition);
|
|
45
|
+
const hashes = [panel?.judgments[0]?.input?.trace_sha256, panel?.objective?.trace_sha256, opts.prior?.objective?.rep_trace_sha256?.[repetition],
|
|
46
|
+
(opts.prior?.reps ?? 1) === 1 ? opts.prior?.objective?.trace_sha256 : undefined].filter((value) => value !== undefined);
|
|
47
|
+
if (new Set(hashes).size > 1)
|
|
48
|
+
throw new Error(`${opts.scenario.id}#${repetition}: conflicting recorded trace digests; regrade refused.`);
|
|
49
|
+
const execution = loadExecutionFacts(tracePath(opts.runDir, opts.scenario.id, mode, rep), {
|
|
50
|
+
scenarioId: opts.scenario.id, mode, repetition, turns: opts.scenario.turns.length,
|
|
51
|
+
subject: opts.subject, piVersion: opts.piVersion, responses: transcriptResponses(transcript, opts.scenario),
|
|
52
|
+
}, hashes[0]);
|
|
53
|
+
if (execution.status === "invalid")
|
|
54
|
+
throw new Error(`${opts.scenario.id}#${repetition}: ${execution.reason} Regrade refused; retained results were not replaced.`);
|
|
55
|
+
return execution;
|
|
56
|
+
}
|
|
37
57
|
/** 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. */
|
|
38
58
|
export async function judgeOneRep(opts) {
|
|
39
59
|
const { runDir, spec, scenario, transcript, adapter, judge, specDir, mode, rep, now } = opts;
|
|
@@ -56,12 +76,30 @@ export async function judgeOneRep(opts) {
|
|
|
56
76
|
metrics: { wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)), judge_calls: 0, judge_rejudge_calls: 0 },
|
|
57
77
|
};
|
|
58
78
|
}
|
|
59
|
-
|
|
79
|
+
if (opts.execution?.status === "invalid") {
|
|
80
|
+
const reason = `execution evidence: ${opts.execution.reason}`;
|
|
81
|
+
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict: "ERROR", reason, suspect: false, ...repField });
|
|
82
|
+
return { verdict: "ERROR", reason, suspect: false, objective: opts.objective,
|
|
83
|
+
metrics: { wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)), judge_calls: 0, judge_rejudge_calls: 0 } };
|
|
84
|
+
}
|
|
85
|
+
const prompt = prepareJudgePrompt({ skill: spec.skill, persona: spec.judge_persona, scenario, transcript, execution: opts.execution, references: opts.references }, specDir);
|
|
60
86
|
const g = await judgeInWorkspace(adapter, judge, prompt, specDir, scenario.checklist.length);
|
|
87
|
+
const inputBytes = JSON.stringify({ schema: 1, judge, execution: opts.execution ?? null, prompts: g.prompts }) + "\n";
|
|
88
|
+
const inputHash = createHash("sha256").update(inputBytes).digest("hex");
|
|
89
|
+
const artifact = `judge-input.${inputHash}.txt`;
|
|
90
|
+
const inputPath = join(runDir, artifact);
|
|
91
|
+
if (existsSync(inputPath)) {
|
|
92
|
+
if (readFileSync(inputPath, "utf8") !== inputBytes)
|
|
93
|
+
throw new Error("judge input artifact has conflicting bytes");
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
writeFileSync(inputPath, inputBytes, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
97
|
+
const input = { artifact, sha256: inputHash, execution: opts.execution?.status === "verified" ? "verified" : "unknown",
|
|
98
|
+
...(opts.execution?.traceSha256 ? { trace_sha256: opts.execution.traceSha256 } : {}) };
|
|
61
99
|
writeFileSync(judgeRawPath(runDir, scenario.id, mode, rep), formatJudgeRawReplies(g.rawReplies), "utf8");
|
|
62
100
|
const outcome = normalizeRepOutcome({
|
|
63
101
|
verdict: g.verdict, reason: g.reason, suspect: g.suspect, objective: opts.objective,
|
|
64
|
-
judgment: { ordinal: 1, judge: { ...judge }, verdict: g.verdict, reason: g.reason, suspect: g.suspect, criteria: completeCriterionVotes(g.criteria, scenario.checklist.length), judgeFormat: g.judgeFormat, ...(g.judgeRetries ? { judgeRetries: g.judgeRetries } : {}) },
|
|
102
|
+
judgment: { ordinal: 1, judge: { ...judge }, verdict: g.verdict, reason: g.reason, suspect: g.suspect, criteria: completeCriterionVotes(g.criteria, scenario.checklist.length), judgeFormat: g.judgeFormat, input, ...(g.judgeRetries ? { judgeRetries: g.judgeRetries } : {}) },
|
|
65
103
|
metrics: {
|
|
66
104
|
wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
67
105
|
judge_calls: 1 + (g.judgeRetries ?? 0),
|
|
@@ -81,6 +119,7 @@ export async function judgeOneRep(opts) {
|
|
|
81
119
|
* persists).
|
|
82
120
|
*/
|
|
83
121
|
export async function regradeScenario(opts) {
|
|
122
|
+
const references = opts.references ?? resolveJudgeReferences(opts.scenario, opts.specDir);
|
|
84
123
|
const now = opts.now ?? (() => new Date().toISOString());
|
|
85
124
|
const mode = opts.mode ?? "green";
|
|
86
125
|
let files = findTranscriptFiles(opts.runDir, opts.scenario.id, mode);
|
|
@@ -109,10 +148,12 @@ export async function regradeScenario(opts) {
|
|
|
109
148
|
const file = files.find(file => (repIndexOf(file) ?? 0) === repetition);
|
|
110
149
|
const rep = repIndexOf(file) ?? undefined;
|
|
111
150
|
const transcript = readFileSync(join(opts.runDir, file), "utf8");
|
|
151
|
+
const execution = savedExecution(opts, mode, rep, transcript);
|
|
112
152
|
outcomes.push(await judgeOneRep({
|
|
113
153
|
runDir: opts.runDir, spec: opts.spec, scenario: opts.scenario, transcript,
|
|
114
154
|
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir, mode, rep, now, rejudge: true,
|
|
115
155
|
objective: panel?.objective ?? (expected === 1 ? opts.prior?.objective : undefined),
|
|
156
|
+
execution, references,
|
|
116
157
|
}));
|
|
117
158
|
}
|
|
118
159
|
const result = outcomesToResult(opts.scenario.id, outcomes, repCount, opts.threshold);
|
|
@@ -138,6 +179,7 @@ function hasUnparsedVotes(panel) {
|
|
|
138
179
|
* ResultsFile. Shared by `cmdGrade` and the pi-extension's `judge` command.
|
|
139
180
|
*/
|
|
140
181
|
export async function regradeRun(opts) {
|
|
182
|
+
throwIfProcessAborted();
|
|
141
183
|
const { runDir, spec, adapter, specDir } = opts;
|
|
142
184
|
const now = opts.now ?? (() => new Date().toISOString());
|
|
143
185
|
const prev = existsSync(join(runDir, "results.yaml")) ? readResults(runDir) : null;
|
|
@@ -169,15 +211,23 @@ export async function regradeRun(opts) {
|
|
|
169
211
|
const specById = new Map(spec.scenarios.map((s) => [s.id, s]));
|
|
170
212
|
const recorded = prev?.scenarios ?? spec.scenarios.map((s) => ({ id: s.id }));
|
|
171
213
|
let targets = recorded.map((s) => s.id);
|
|
214
|
+
if (opts.only !== undefined) {
|
|
215
|
+
if (!opts.only.length || opts.only.some(id => !id.trim()) || new Set(opts.only).size !== opts.only.length)
|
|
216
|
+
throw new Error("Selected scenarios must be a nonempty unique ID list");
|
|
217
|
+
const unknown = opts.only.filter(id => !targets.includes(id));
|
|
218
|
+
if (unknown.length)
|
|
219
|
+
throw new Error(`Unknown recorded scenario(s): ${unknown.join(", ")}`);
|
|
220
|
+
targets = targets.filter(id => opts.only.includes(id));
|
|
221
|
+
}
|
|
172
222
|
if (opts.onlySuspect) {
|
|
173
223
|
if (!prev)
|
|
174
224
|
throw new Error(`--suspect-only needs a prior results.yaml in ${runDir}`);
|
|
175
|
-
targets = prev.scenarios
|
|
225
|
+
targets = prev.scenarios.filter(s => targets.includes(s.id))
|
|
176
226
|
.filter((s) => s.suspect || s.judge_verdict === "JUDGE-AMBIGUOUS")
|
|
177
227
|
.map((s) => s.id);
|
|
178
228
|
}
|
|
179
229
|
if (opts.onlyUnparsed)
|
|
180
|
-
targets = recorded.filter(s => s.rep_judgments?.some(hasUnparsedVotes)).map(s => s.id);
|
|
230
|
+
targets = recorded.filter(s => targets.includes(s.id) && s.rep_judgments?.some(hasUnparsedVotes)).map(s => s.id);
|
|
181
231
|
// Schema-3 delivery is a prerequisite for behavioral judging. A later `grade`
|
|
182
232
|
// cannot turn an undelivered or unauthenticated transcript into product evidence.
|
|
183
233
|
if (prev?.schema === 3) {
|
|
@@ -188,6 +238,8 @@ export async function regradeRun(opts) {
|
|
|
188
238
|
// Nothing judgeable — a no-op, preserving both evidence and judge identity.
|
|
189
239
|
return prev;
|
|
190
240
|
}
|
|
241
|
+
if (prev && targets.length !== recorded.length && (judge.provider !== prev.judge.provider || judge.model !== prev.judge.model))
|
|
242
|
+
throw new Error("Selective regrade must use the recorded judge; regrade the whole run to change judge");
|
|
191
243
|
const completeTranscripts = (record) => {
|
|
192
244
|
const expected = record.reps ?? 1;
|
|
193
245
|
const files = findTranscriptFiles(runDir, record.id, mode);
|
|
@@ -214,73 +266,92 @@ export async function regradeRun(opts) {
|
|
|
214
266
|
if (drifted.length)
|
|
215
267
|
throw new Error(`criterion count changed for ${drifted.join(", ")} — use full grade instead of --unparsed-only`);
|
|
216
268
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
269
|
+
// Refuse the whole selected batch before spending on any earlier cell. Recheck
|
|
270
|
+
// again immediately before each call so a later filesystem edit is not trusted.
|
|
271
|
+
for (const id of targets) {
|
|
272
|
+
const prior = recordedById.get(id);
|
|
273
|
+
for (const file of findTranscriptFiles(runDir, id, mode)) {
|
|
274
|
+
const rep = repIndexOf(file) ?? undefined;
|
|
275
|
+
if (opts.onlyUnparsed && !hasUnparsedVotes(prior.rep_judgments?.find(panel => panel.repetition === (rep ?? 0))))
|
|
276
|
+
continue;
|
|
277
|
+
savedExecution({ runDir, scenario: specById.get(id), prior,
|
|
278
|
+
...(prev?.model.includes(":") ? { subject: parseModelRef(prev.model) } : {}), piVersion: prev?.harness_cli_version, }, mode, rep, readFileSync(join(runDir, file), "utf8"));
|
|
224
279
|
}
|
|
225
|
-
const scenario = specById.get(id); // guaranteed present by the guard above
|
|
226
|
-
const prevScenario = prev?.scenarios.find((s) => s.id === id);
|
|
227
|
-
const threshold = effectiveThreshold(prevScenario, scenario);
|
|
228
|
-
const rr = await regradeScenario({
|
|
229
|
-
runDir, spec, scenario, adapter, judge, specDir, threshold, mode, now,
|
|
230
|
-
expectedReps: prevScenario?.reps ?? 1, onlyUnparsed: opts.onlyUnparsed, prior: prevScenario,
|
|
231
|
-
});
|
|
232
|
-
const carry = overrides.get(id);
|
|
233
|
-
if (prev?.schema === 3)
|
|
234
|
-
rr.criterion_count = scenario.checklist.length;
|
|
235
|
-
rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
|
|
236
|
-
rr.rep_judgments = carryRepObjectives(rr.rep_judgments, carry?.rep_judgments);
|
|
237
|
-
// Gates still describe the original subject execution. An adjudication of
|
|
238
|
-
// an untouched repetition also survives, bounded by rebuild's conflict guards.
|
|
239
|
-
const carryAdjudication = opts.onlyUnparsed && carry?.adjudication && !hasUnparsedVotes(carry.rep_judgments?.find(panel => panel.repetition === (carry.adjudication.repetition ?? 0)));
|
|
240
|
-
scenarioResults.push(rebuildScenarioResult(rr, carry, { objective: "carry", adjudication: carryAdjudication ? "carry" : "drop" }));
|
|
241
|
-
}
|
|
242
|
-
const ctx = scoreContextFor({ mode, partial: prev?.partial }, spec);
|
|
243
|
-
const results = writeResults(runDir, {
|
|
244
|
-
schema: prev?.schema,
|
|
245
|
-
subject_invocations: prev?.subject_invocations,
|
|
246
|
-
skill: spec.skill,
|
|
247
|
-
harness: prev?.harness ?? "pi",
|
|
248
|
-
// The harness CLI that produced these transcripts, carried verbatim: a re-grade
|
|
249
|
-
// re-asks the judge, it does not re-deliver the skill, so stamping today's pi
|
|
250
|
-
// here would credit the old transcripts to a version that never ran them.
|
|
251
|
-
harness_cli_version: prev?.harness_cli_version,
|
|
252
|
-
delivery_canary: prev?.delivery_canary,
|
|
253
|
-
// The arm is provenance of the MEASUREMENT, not of this rewrite, and it is the
|
|
254
|
-
// only record that a `+<arm>` run actually delegated: rebuilding the draft
|
|
255
|
-
// field-by-field without it silently deleted `definitions`/`ledger_events`
|
|
256
|
-
// from any arm run that was ever re-graded, leaving a record
|
|
257
|
-
// indistinguishable from a vacuous arm. Same reason `harness_cli_version`,
|
|
258
|
-
// `delivery_canary` and `source_hashes` are carried here.
|
|
259
|
-
arm: prev?.arm,
|
|
260
|
-
model: prev?.model ?? "unknown",
|
|
261
|
-
judge: { provider: judge.provider, model: judge.model },
|
|
262
|
-
timestamp: prev?.timestamp ?? now(),
|
|
263
|
-
label: prev?.label ?? null,
|
|
264
|
-
mode,
|
|
265
|
-
// A re-grade judges the SAVED transcripts, which were produced by the OLD text —
|
|
266
|
-
// the recorded **stimulus** hashes stay, keeping an honestly-stale run honestly
|
|
267
|
-
// stale. The rubric hashes are a different matter: this re-grade applied the
|
|
268
|
-
// CURRENT checklist and persona to those transcripts, so "the verdicts reflect
|
|
269
|
-
// today's rubric" is now a true statement about the record, and the hashes should
|
|
270
|
-
// say so. Doctrine narrowed 0.4.0, from "recorded hashes stay" to "recorded
|
|
271
|
-
// *stimulus* hashes stay" — see refreshRubricHashes.
|
|
272
|
-
partial: prev?.partial,
|
|
273
|
-
source_hashes: opts.onlyUnparsed ? prev?.source_hashes : refreshRubricHashes(prev?.source_hashes, spec, targets),
|
|
274
|
-
source_hash_roots: prev?.source_hash_roots,
|
|
275
|
-
scenarios: scenarioResults,
|
|
276
|
-
}, ctx);
|
|
277
|
-
const g = results.effective_grade;
|
|
278
|
-
if (ctx) {
|
|
279
|
-
appendJournal(runDir, {
|
|
280
|
-
event: "score", ts: now(),
|
|
281
|
-
passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note,
|
|
282
|
-
});
|
|
283
280
|
}
|
|
284
|
-
|
|
281
|
+
const references = Object.fromEntries(targets.map(id => [id, resolveJudgeReferences(specById.get(id), specDir)]));
|
|
282
|
+
return withResourceMonitoring(runDir, async () => {
|
|
283
|
+
const targetSet = new Set(targets);
|
|
284
|
+
const scenarioResults = [];
|
|
285
|
+
for (const rec of recorded) {
|
|
286
|
+
throwIfProcessAborted();
|
|
287
|
+
const id = rec.id;
|
|
288
|
+
if (!targetSet.has(id)) {
|
|
289
|
+
scenarioResults.push(rec); // clean verdict, carried verbatim (onlySuspect)
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const scenario = specById.get(id); // guaranteed present by the guard above
|
|
293
|
+
const prevScenario = prev?.scenarios.find((s) => s.id === id);
|
|
294
|
+
const threshold = effectiveThreshold(prevScenario, scenario);
|
|
295
|
+
const rr = await regradeScenario({
|
|
296
|
+
runDir, spec, scenario, adapter, judge, specDir, threshold, mode, now,
|
|
297
|
+
references: references[id],
|
|
298
|
+
expectedReps: prevScenario?.reps ?? 1, onlyUnparsed: opts.onlyUnparsed, prior: prevScenario,
|
|
299
|
+
...(prev?.model.includes(":") ? { subject: parseModelRef(prev.model) } : {}), piVersion: prev?.harness_cli_version,
|
|
300
|
+
});
|
|
301
|
+
const carry = overrides.get(id);
|
|
302
|
+
if (prev?.schema === 3)
|
|
303
|
+
rr.criterion_count = scenario.checklist.length;
|
|
304
|
+
rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
|
|
305
|
+
rr.rep_judgments = carryRepObjectives(rr.rep_judgments, carry?.rep_judgments);
|
|
306
|
+
// Gates still describe the original subject execution. An adjudication of
|
|
307
|
+
// an untouched repetition also survives, bounded by rebuild's conflict guards.
|
|
308
|
+
const carryAdjudication = opts.onlyUnparsed && carry?.adjudication && !hasUnparsedVotes(carry.rep_judgments?.find(panel => panel.repetition === (carry.adjudication.repetition ?? 0)));
|
|
309
|
+
scenarioResults.push(rebuildScenarioResult(rr, carry, { objective: "carry", adjudication: carryAdjudication ? "carry" : "drop" }));
|
|
310
|
+
}
|
|
311
|
+
const ctx = scoreContextFor({ mode, partial: prev?.partial }, spec);
|
|
312
|
+
const results = writeResults(runDir, {
|
|
313
|
+
schema: prev?.schema,
|
|
314
|
+
subject_invocations: prev?.subject_invocations,
|
|
315
|
+
skill: spec.skill,
|
|
316
|
+
harness: prev?.harness ?? "pi",
|
|
317
|
+
// The harness CLI that produced these transcripts, carried verbatim: a re-grade
|
|
318
|
+
// re-asks the judge, it does not re-deliver the skill, so stamping today's pi
|
|
319
|
+
// here would credit the old transcripts to a version that never ran them.
|
|
320
|
+
harness_cli_version: prev?.harness_cli_version,
|
|
321
|
+
delivery_canary: prev?.delivery_canary,
|
|
322
|
+
delivery_observations: prev?.delivery_observations,
|
|
323
|
+
// The arm is provenance of the MEASUREMENT, not of this rewrite, and it is the
|
|
324
|
+
// only record that a `+<arm>` run actually delegated: rebuilding the draft
|
|
325
|
+
// field-by-field without it silently deleted `definitions`/`ledger_events`
|
|
326
|
+
// from any arm run that was ever re-graded, leaving a record
|
|
327
|
+
// indistinguishable from a vacuous arm. Same reason `harness_cli_version`,
|
|
328
|
+
// `delivery_canary` and `source_hashes` are carried here.
|
|
329
|
+
arm: prev?.arm,
|
|
330
|
+
model: prev?.model ?? "unknown",
|
|
331
|
+
judge: { provider: judge.provider, model: judge.model },
|
|
332
|
+
timestamp: prev?.timestamp ?? now(),
|
|
333
|
+
label: prev?.label ?? null,
|
|
334
|
+
mode,
|
|
335
|
+
// A re-grade judges the SAVED transcripts, which were produced by the OLD text —
|
|
336
|
+
// the recorded **stimulus** hashes stay, keeping an honestly-stale run honestly
|
|
337
|
+
// stale. The rubric hashes are a different matter: this re-grade applied the
|
|
338
|
+
// CURRENT checklist and persona to those transcripts, so "the verdicts reflect
|
|
339
|
+
// today's rubric" is now a true statement about the record, and the hashes should
|
|
340
|
+
// say so. Doctrine narrowed 0.4.0, from "recorded hashes stay" to "recorded
|
|
341
|
+
// *stimulus* hashes stay" — see refreshRubricHashes.
|
|
342
|
+
partial: prev?.partial,
|
|
343
|
+
source_hashes: opts.onlyUnparsed ? prev?.source_hashes : refreshRubricHashes(prev?.source_hashes, spec, targets, opts.specDir, references, targets.length === recorded.length),
|
|
344
|
+
source_hash_roots: prev?.source_hash_roots,
|
|
345
|
+
scenarios: scenarioResults,
|
|
346
|
+
}, ctx);
|
|
347
|
+
const g = results.effective_grade;
|
|
348
|
+
if (ctx) {
|
|
349
|
+
appendJournal(runDir, {
|
|
350
|
+
event: "score", ts: now(),
|
|
351
|
+
passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return results;
|
|
355
|
+
});
|
|
285
356
|
}
|
|
286
357
|
//# sourceMappingURL=regrade.js.map
|
package/dist/rescore.js
CHANGED
|
@@ -95,6 +95,7 @@ export function rescoreRun(opts) {
|
|
|
95
95
|
// threshold to reps the recorded harness already produced.
|
|
96
96
|
harness_cli_version: prev.harness_cli_version,
|
|
97
97
|
delivery_canary: prev.delivery_canary,
|
|
98
|
+
delivery_observations: prev.delivery_observations,
|
|
98
99
|
// The arm is provenance of the MEASUREMENT, not of this rewrite, and it is the
|
|
99
100
|
// only record that a `+<arm>` run actually delegated: rebuilding the draft
|
|
100
101
|
// field-by-field without it silently deleted `definitions`/`ledger_events`
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ProcessScope } from "./util/process.js";
|
|
2
|
+
/** Per-operation metadata only, with bounded retention and low-frequency atomic snapshots. */
|
|
3
|
+
export declare function withResourceMonitoring<T>(directory: string, work: () => Promise<T>, options?: ProcessScope): Promise<T>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getProcessSignal, withProcessScope } from "./util/process.js";
|
|
5
|
+
/** Per-operation metadata only, with bounded retention and low-frequency atomic snapshots. */
|
|
6
|
+
export async function withResourceMonitoring(directory, work, options = {}) {
|
|
7
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
8
|
+
const id = randomUUID();
|
|
9
|
+
const path = join(directory, `resources-${id}.json`), pending = path + ".tmp";
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const inherited = options.signal ?? getProcessSignal();
|
|
12
|
+
const abort = () => controller.abort(inherited?.reason);
|
|
13
|
+
if (inherited?.aborted)
|
|
14
|
+
abort();
|
|
15
|
+
inherited?.addEventListener("abort", abort, { once: true });
|
|
16
|
+
const records = [];
|
|
17
|
+
const active = new Map();
|
|
18
|
+
let total = 0, dropped = 0, droppedActive = 0, state = "running", flushError;
|
|
19
|
+
const started = new Date().toISOString();
|
|
20
|
+
const flush = () => {
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(pending, JSON.stringify({ schema: 1, scope: "harness-owned-processes", started, updated: new Date().toISOString(), state,
|
|
23
|
+
hostHealth: "unmeasured", sampling: "one-second-owned-tree", totalProcesses: total, droppedObservations: dropped, droppedActiveObservations: droppedActive, active: [...active.values()], observations: records }, null, 2) + "\n", { mode: 0o600 });
|
|
24
|
+
renameSync(pending, path);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
flushError = error;
|
|
28
|
+
controller.abort(error);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
flush();
|
|
32
|
+
if (flushError) {
|
|
33
|
+
inherited?.removeEventListener("abort", abort);
|
|
34
|
+
throw flushError;
|
|
35
|
+
}
|
|
36
|
+
const timer = setInterval(flush, 5000);
|
|
37
|
+
timer.unref();
|
|
38
|
+
try {
|
|
39
|
+
return await withProcessScope({ ...options, signal: controller.signal, onSample: record => {
|
|
40
|
+
if (record.pid !== null && (active.has(record.pid) || active.size < 256))
|
|
41
|
+
active.set(record.pid, record);
|
|
42
|
+
else
|
|
43
|
+
droppedActive++;
|
|
44
|
+
options.onSample?.(record);
|
|
45
|
+
}, onObservation: record => {
|
|
46
|
+
total++;
|
|
47
|
+
if (record.pid !== null)
|
|
48
|
+
active.delete(record.pid);
|
|
49
|
+
if (records.length < 256)
|
|
50
|
+
records.push(record);
|
|
51
|
+
else
|
|
52
|
+
dropped++;
|
|
53
|
+
if (record.reason === "resource-limit" || record.reason === "output-limit")
|
|
54
|
+
controller.abort(new Error(record.reason));
|
|
55
|
+
options.onObservation?.(record);
|
|
56
|
+
} }, work);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
state = "interrupted";
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
clearInterval(timer);
|
|
64
|
+
inherited?.removeEventListener("abort", abort);
|
|
65
|
+
if (state === "running")
|
|
66
|
+
state = "settled";
|
|
67
|
+
flush();
|
|
68
|
+
if (flushError)
|
|
69
|
+
throw new Error(`Resource summary could not be retained: ${String(flushError)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=resource-monitor.js.map
|
package/dist/results.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ScenarioVerdict } from "./score.js";
|
|
|
3
3
|
import type { Verdict } from "./score.js";
|
|
4
4
|
import type { ShipBar, Scenario } from "./spec.js";
|
|
5
5
|
import type { CostSource } from "./capture-trace-types.js";
|
|
6
|
+
import type { CanaryObservations } from "./canary.js";
|
|
6
7
|
export interface SubjectUsage {
|
|
7
8
|
repetition: number;
|
|
8
9
|
inputTokens: number | null;
|
|
@@ -105,6 +106,13 @@ export interface Judgment {
|
|
|
105
106
|
judgeFormat?: "json" | "prose";
|
|
106
107
|
/** One validation-guided retry after an invalid structured reply. */
|
|
107
108
|
judgeRetries?: 1;
|
|
109
|
+
/** Content-addressed private input artifact; preserves exact prompts and execution coverage. */
|
|
110
|
+
input?: {
|
|
111
|
+
artifact: string;
|
|
112
|
+
sha256: string;
|
|
113
|
+
execution: "verified" | "unknown";
|
|
114
|
+
trace_sha256?: string;
|
|
115
|
+
};
|
|
108
116
|
}
|
|
109
117
|
export interface AdjudicationResult {
|
|
110
118
|
/** The saved transcript repetition judged by every panel member (first rep by policy). */
|
|
@@ -174,10 +182,10 @@ export interface ResultsFile {
|
|
|
174
182
|
*/
|
|
175
183
|
harness_cli_version?: string;
|
|
176
184
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
185
|
+
* Compatibility status for the optional instructed loading/quote probe.
|
|
186
|
+
* See delivery_observations for what was actually observed; a quote by itself
|
|
187
|
+
* does not establish loading, and the probe never tests autonomous selection.
|
|
188
|
+
* `skipped` means the skill has no `## ` heading. Absent means the probe was not asked
|
|
181
189
|
* for — never that it failed, because a failed canary aborts the run and no
|
|
182
190
|
* results.yaml is written.
|
|
183
191
|
*
|
|
@@ -192,6 +200,10 @@ export interface ResultsFile {
|
|
|
192
200
|
* gitignored, and this claim has to survive a commit.
|
|
193
201
|
*/
|
|
194
202
|
delivery_canary?: "pass" | "skipped";
|
|
203
|
+
/** Optional for historical records; never inferred from a legacy pass alone. */
|
|
204
|
+
delivery_observations?: CanaryObservations & {
|
|
205
|
+
probeCalls: 0 | 1;
|
|
206
|
+
};
|
|
195
207
|
skill: string;
|
|
196
208
|
harness: string;
|
|
197
209
|
model: string;
|
package/dist/results.js
CHANGED
|
@@ -224,6 +224,7 @@ export function finalizeResults(draft, ctx) {
|
|
|
224
224
|
// not report a version must not look like one that reported "nothing".
|
|
225
225
|
...(draft.harness_cli_version ? { harness_cli_version: draft.harness_cli_version } : {}),
|
|
226
226
|
...(draft.delivery_canary ? { delivery_canary: draft.delivery_canary } : {}),
|
|
227
|
+
...(draft.delivery_observations ? { delivery_observations: draft.delivery_observations } : {}),
|
|
227
228
|
skill: draft.skill,
|
|
228
229
|
harness: draft.harness,
|
|
229
230
|
model: draft.model,
|
|
@@ -498,6 +499,9 @@ export function applyOverride(results, scenarioId, override, note) {
|
|
|
498
499
|
const GITIGNORE_BODY = `# skill-harness: commit verdicts (results.yaml), ignore generated artifacts.
|
|
499
500
|
*.txt
|
|
500
501
|
*.jsonl
|
|
502
|
+
resources-*.json
|
|
503
|
+
resources-*.json.tmp
|
|
504
|
+
reuse-source-*.json
|
|
501
505
|
report.html
|
|
502
506
|
!results.yaml
|
|
503
507
|
`;
|
|
@@ -743,6 +747,11 @@ export function findTranscriptFile(runDir, scenarioId) {
|
|
|
743
747
|
* for that claim is the code the model wrote.
|
|
744
748
|
*/
|
|
745
749
|
export function preserveTranscript(resultsRoot, runDir, scenarioId) {
|
|
750
|
+
const record = existsSync(resultsPath(runDir)) ? readResults(runDir).scenarios.find(scenario => scenario.id === scenarioId) : undefined;
|
|
751
|
+
const judgeInputs = (record?.rep_judgments ?? []).flatMap(panel => panel.judgments.flatMap(judgment => {
|
|
752
|
+
const input = judgment.input;
|
|
753
|
+
return input && input.artifact === `judge-input.${input.sha256}.txt` && /^[a-f0-9]{64}$/.test(input.sha256) && existsSync(join(runDir, input.artifact)) ? [input.artifact] : [];
|
|
754
|
+
}));
|
|
746
755
|
const files = [
|
|
747
756
|
...findTranscriptFiles(runDir, scenarioId),
|
|
748
757
|
...findJudgeRawFiles(runDir, scenarioId),
|
|
@@ -752,6 +761,7 @@ export function preserveTranscript(resultsRoot, runDir, scenarioId) {
|
|
|
752
761
|
// override whose justification was gitignored, and left `regate` with nothing
|
|
753
762
|
// to re-evaluate on the one cell a human had disputed.
|
|
754
763
|
...findTraceFiles(runDir, scenarioId),
|
|
764
|
+
...judgeInputs,
|
|
755
765
|
];
|
|
756
766
|
if (files.length === 0)
|
|
757
767
|
return;
|
package/dist/run.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export interface RunOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
only?: string[];
|
|
33
33
|
/**
|
|
34
|
-
* Green mode only: spend ONE probe up front
|
|
35
|
-
* and
|
|
34
|
+
* Green mode only: spend ONE instructed loading/read/quote probe up front,
|
|
35
|
+
* and stop the wave if inconclusive (see canary.ts). Off by default — it costs a
|
|
36
36
|
* rep, and the deterministic half of this failure class (a skill dir that isn't
|
|
37
37
|
* there) is already refused by the adapter for free.
|
|
38
38
|
*/
|