@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.
Files changed (59) hide show
  1. package/README.md +16 -2
  2. package/dist/adapters/types.d.ts +2 -0
  3. package/dist/authoring-preflight.d.ts +22 -0
  4. package/dist/authoring-preflight.js +111 -0
  5. package/dist/authoring.d.ts +96 -0
  6. package/dist/authoring.js +451 -0
  7. package/dist/calibration.d.ts +138 -0
  8. package/dist/calibration.js +352 -0
  9. package/dist/canary.d.ts +28 -26
  10. package/dist/canary.js +109 -92
  11. package/dist/command-cost.d.ts +1 -1
  12. package/dist/command-cost.js +2 -0
  13. package/dist/diagnose.d.ts +17 -0
  14. package/dist/diagnose.js +70 -0
  15. package/dist/execution-trace.d.ts +2 -1
  16. package/dist/execution-trace.js +4 -3
  17. package/dist/grade.d.ts +8 -0
  18. package/dist/grade.js +37 -8
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.js +8 -0
  21. package/dist/journal.d.ts +3 -0
  22. package/dist/judge-evidence.d.ts +40 -0
  23. package/dist/judge-evidence.js +120 -0
  24. package/dist/judge-references.d.ts +8 -0
  25. package/dist/judge-references.js +34 -0
  26. package/dist/lint.d.ts +1 -1
  27. package/dist/lint.js +22 -0
  28. package/dist/regate.js +21 -3
  29. package/dist/regrade.d.ts +10 -1
  30. package/dist/regrade.js +146 -75
  31. package/dist/rescore.js +1 -0
  32. package/dist/resource-monitor.d.ts +3 -0
  33. package/dist/resource-monitor.js +72 -0
  34. package/dist/results.d.ts +16 -4
  35. package/dist/results.js +10 -0
  36. package/dist/run.d.ts +2 -2
  37. package/dist/run.js +147 -116
  38. package/dist/saved-run.d.ts +15 -0
  39. package/dist/saved-run.js +114 -0
  40. package/dist/scheduler.d.ts +3 -5
  41. package/dist/scheduler.js +18 -6
  42. package/dist/seeded.js +1 -0
  43. package/dist/sources.d.ts +4 -1
  44. package/dist/sources.js +44 -40
  45. package/dist/spec.d.ts +8 -0
  46. package/dist/spec.js +27 -0
  47. package/dist/trace-gates.d.ts +2 -0
  48. package/dist/trace-gates.js +13 -3
  49. package/dist/util/exec.d.ts +5 -0
  50. package/dist/util/exec.js +16 -32
  51. package/dist/util/fixture-inventory.d.ts +19 -0
  52. package/dist/util/fixture-inventory.js +68 -0
  53. package/dist/util/process.d.ts +78 -0
  54. package/dist/util/process.js +394 -0
  55. package/dist/workspace.js +13 -29
  56. package/package.json +1 -1
  57. package/schemas/results-v2.schema.json +10 -0
  58. package/schemas/results-v3.schema.json +10 -0
  59. 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 { buildJudgePrompt, formatJudgeRawReplies, judgeInWorkspace } from "./grade.js";
4
- import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, completeCriterionVotes, } from "./results.js";
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
- const prompt = buildJudgePrompt({ skill: spec.skill, persona: spec.judge_persona, scenario, transcript });
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
- const targetSet = new Set(targets);
218
- const scenarioResults = [];
219
- for (const rec of recorded) {
220
- const id = rec.id;
221
- if (!targetSet.has(id)) {
222
- scenarioResults.push(rec); // clean verdict, carried verbatim (onlySuspect)
223
- continue;
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
- return results;
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
- * `pass` when this run proved, before spending the wave, that the skill body was
178
- * reachable in the model's context (see canary.ts). `skipped` when the probe was
179
- * asked for but could not be performed — SKILL.md has no `## ` heading to quote
180
- * back, so no reply could prove anything. Absent means the probe was not asked
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 proving the skill reaches the model,
35
- * and abort the run if it doesn't (see canary.ts). Off by default — it costs a
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
  */