@skill-harness/core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +2 -1
  2. package/dist/adapters/types.d.ts +19 -0
  3. package/dist/adjudication.d.ts +9 -1
  4. package/dist/adjudication.js +41 -33
  5. package/dist/arms.d.ts +1 -1
  6. package/dist/capture-trace-types.d.ts +2 -0
  7. package/dist/command-cost.d.ts +9 -0
  8. package/dist/command-cost.js +23 -0
  9. package/dist/comparison.d.ts +1 -0
  10. package/dist/comparison.js +9 -3
  11. package/dist/execution-trace.js +9 -1
  12. package/dist/grade.d.ts +2 -0
  13. package/dist/grade.js +2 -1
  14. package/dist/index.d.ts +12 -0
  15. package/dist/index.js +12 -0
  16. package/dist/journal.d.ts +1 -1
  17. package/dist/judge-agreement.d.ts +19 -0
  18. package/dist/judge-agreement.js +31 -0
  19. package/dist/judge-policy.js +5 -3
  20. package/dist/lift.js +1 -1
  21. package/dist/lint.js +1 -1
  22. package/dist/prompt-normalization.d.ts +6 -0
  23. package/dist/prompt-normalization.js +14 -0
  24. package/dist/qualification-capture.d.ts +44 -0
  25. package/dist/qualification-capture.js +163 -0
  26. package/dist/qualification-config.d.ts +233 -0
  27. package/dist/qualification-config.js +694 -0
  28. package/dist/qualification-lock.d.ts +15 -0
  29. package/dist/qualification-lock.js +87 -0
  30. package/dist/qualification-oauth-directory.d.ts +79 -0
  31. package/dist/qualification-oauth-directory.js +444 -0
  32. package/dist/qualification-panel-store.d.ts +14 -0
  33. package/dist/qualification-panel-store.js +188 -0
  34. package/dist/qualification-panels.d.ts +99 -0
  35. package/dist/qualification-panels.js +91 -0
  36. package/dist/qualification-process.d.ts +13 -0
  37. package/dist/qualification-process.js +188 -0
  38. package/dist/qualification-runner.d.ts +203 -0
  39. package/dist/qualification-runner.js +2037 -0
  40. package/dist/qualification-store.d.ts +187 -0
  41. package/dist/qualification-store.js +833 -0
  42. package/dist/qualification-validation.d.ts +12 -0
  43. package/dist/qualification-validation.js +8 -0
  44. package/dist/regate.js +42 -10
  45. package/dist/regrade.d.ts +3 -1
  46. package/dist/regrade.js +37 -5
  47. package/dist/reps.d.ts +4 -2
  48. package/dist/reps.js +13 -4
  49. package/dist/rescore.js +2 -1
  50. package/dist/results.d.ts +53 -7
  51. package/dist/results.js +210 -7
  52. package/dist/run.js +66 -17
  53. package/dist/score.d.ts +4 -3
  54. package/dist/score.js +13 -4
  55. package/dist/screen.d.ts +31 -0
  56. package/dist/screen.js +105 -0
  57. package/dist/seeded.d.ts +2 -1
  58. package/dist/seeded.js +1 -0
  59. package/dist/sources.d.ts +7 -1
  60. package/dist/sources.js +13 -5
  61. package/dist/spec.d.ts +1 -1
  62. package/dist/spec.js +1 -1
  63. package/dist/stability.js +1 -1
  64. package/dist/trajectory-gates.d.ts +14 -2
  65. package/dist/trajectory-gates.js +26 -6
  66. package/dist/vote-panel.d.ts +33 -0
  67. package/dist/vote-panel.js +41 -0
  68. package/dist/workspace.js +4 -1
  69. package/package.json +3 -3
  70. package/schemas/qualification-cell-result-v1.schema.json +32 -0
  71. package/schemas/qualification-config-v1.schema.json +244 -0
  72. package/schemas/qualification-invocation-request-v1.schema.json +59 -0
  73. package/schemas/qualification-judge-panel-result-v1.schema.json +42 -0
  74. package/schemas/results-v2.schema.json +19 -21
  75. package/schemas/results-v3.schema.json +654 -0
  76. package/schemas/specification-v1.schema.json +1 -1
  77. package/schemas/trajectory-event-v1.schema.json +20 -1
@@ -0,0 +1,12 @@
1
+ /** Canonical final validator: invocation evidence plus panel-board completeness. */
2
+ export declare function validateQualificationRunnerSpool(spoolDir: string): {
3
+ derived: {
4
+ panels: number;
5
+ cells: number;
6
+ };
7
+ ok: true;
8
+ configuration_sha256: string;
9
+ invocations: number;
10
+ accounting: import("./qualification-store.js").QualificationAccountingReport;
11
+ terminal: number;
12
+ };
@@ -0,0 +1,8 @@
1
+ import { validateQualificationPanelOutputs } from "./qualification-panel-store.js";
2
+ import { validateQualificationBaseSpool } from "./qualification-runner.js";
3
+ /** Canonical final validator: invocation evidence plus panel-board completeness. */
4
+ export function validateQualificationRunnerSpool(spoolDir) {
5
+ const base = validateQualificationBaseSpool(spoolDir);
6
+ return { ...base, derived: validateQualificationPanelOutputs(spoolDir) };
7
+ }
8
+ //# sourceMappingURL=qualification-validation.js.map
package/dist/regate.js CHANGED
@@ -6,7 +6,7 @@ import { evaluateTraceGates } from "./trace-gates.js";
6
6
  import { mergeTraces, deserializeTrace } from "./execution-trace.js";
7
7
  import { deserializeTrajectoryEvents, evaluateTrajectoryGates } from "./trajectory-gates.js";
8
8
  import { judgeOneRep } from "./regrade.js";
9
- import { readResults, writeResults, diffPath, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, findTrajectoryFiles, tracePath, trajectoryPath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, } from "./results.js";
9
+ import { readResults, writeResults, diffPath, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, findTrajectoryFiles, tracePath, trajectoryPath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, deliveryStatusForObservations, } from "./results.js";
10
10
  import { outcomesToResult } from "./reps.js";
11
11
  import { appendJournal } from "./journal.js";
12
12
  import { gatesDigest, GATES_PREFIX } from "./sources.js";
@@ -222,9 +222,11 @@ export async function regateRun(opts) {
222
222
  const priorStatus = objective?.status ?? "PASS";
223
223
  const status = priorStatus === "ERROR" || g.status === "ERROR" || priorEvidenceErrors.length || digestMismatch
224
224
  ? "ERROR"
225
- : priorStatus === "FAIL" || g.status === "FAIL"
226
- ? "FAIL"
227
- : "PASS";
225
+ : priorStatus === "NOT-MEASURED"
226
+ ? "NOT-MEASURED"
227
+ : priorStatus === "FAIL" || g.status === "FAIL"
228
+ ? "FAIL"
229
+ : "PASS";
228
230
  objective = {
229
231
  ...(objective ?? {}),
230
232
  status,
@@ -242,6 +244,21 @@ export async function regateRun(opts) {
242
244
  }
243
245
  }
244
246
  }
247
+ // Prompt delivery was observed during the subject invocation and cannot be
248
+ // replayed by regate. Carry its objective finding while refreshing only
249
+ // trace/trajectory/needle assertions.
250
+ const retainedDelivery = rec.objective?.assertions.find(assertion => assertion.kind === "skill_delivered");
251
+ const repObservations = prev.subject_invocations?.filter(observation => observation.scenario_id === scenario.id && observation.repetition === (rep ?? 0)) ?? [];
252
+ const delivery = prev.schema === 3
253
+ ? { kind: "skill_delivered", status: deliveryStatusForObservations(repObservations), detail: `${repObservations.length} retained provider request observation(s)` }
254
+ : retainedDelivery;
255
+ if (delivery) {
256
+ const prior = objective?.status ?? "PASS";
257
+ const status = delivery.status === "ERROR" || prior === "ERROR" ? "ERROR" : delivery.status === "NOT-MEASURED" ? "NOT-MEASURED" : prior === "FAIL" ? "FAIL" : "PASS";
258
+ objective = { ...(objective ?? {}), status, assertions: [delivery, ...(objective?.assertions ?? [])] };
259
+ if (delivery.status !== "PASS")
260
+ traceFailure = `objective: ${delivery.detail}`;
261
+ }
245
262
  const gate = { lines: needleGate.lines, failure: needleGate.failure ?? traceFailure ?? trajectoryFailure };
246
263
  const tPath = transcriptPath(opts.runDir, scenario.id, mode, rep);
247
264
  const before = existsSync(tPath) ? readFileSync(tPath, "utf8") : "";
@@ -250,7 +267,7 @@ export async function regateRun(opts) {
250
267
  // trace gate leaves an `objective` block on the result. Reading only the
251
268
  // trailer meant a trace gate flipping to PASS never triggered the re-judge
252
269
  // it needs, leaving a stale FAIL verdict beside a PASS objective.
253
- const oldObjectiveFailed = rec.objective?.status === "FAIL" || rec.objective?.status === "ERROR";
270
+ const oldObjectiveFailed = rec.objective?.status === "FAIL" || rec.objective?.status === "ERROR" || rec.objective?.status === "NOT-MEASURED";
254
271
  const oldGateFailed = GATE_FAILED_RE.test(before.slice(before.indexOf(TRAILER))) || oldObjectiveFailed;
255
272
  // The trailer is regenerated whatever the outcome: leaving a stale
256
273
  // `MISSING` note beside a corrected verdict would misinform the next reader
@@ -258,11 +275,11 @@ export async function regateRun(opts) {
258
275
  if (existsSync(tPath))
259
276
  rewriteTranscript(tPath, gate.lines);
260
277
  if (gate.failure) {
261
- if (objective?.status === "ERROR")
278
+ if (objective?.status === "ERROR" || objective?.status === "NOT-MEASURED")
262
279
  gateErroredHere = true;
263
280
  else
264
281
  gateFailedHere = true;
265
- outcomes.push({ verdict: objective?.status === "ERROR" ? "ERROR" : "FAIL", reason: gate.failure, suspect: false, objective });
282
+ outcomes.push({ verdict: objective?.status === "ERROR" ? "ERROR" : objective?.status === "NOT-MEASURED" ? "NOT-MEASURED" : "FAIL", reason: gate.failure, suspect: false, objective });
266
283
  continue;
267
284
  }
268
285
  if (!oldGateFailed) {
@@ -284,12 +301,26 @@ export async function regateRun(opts) {
284
301
  }
285
302
  const threshold = effectiveThreshold(rec, scenario);
286
303
  const next = outcomesToResult(scenario.id, outcomes, outcomes.length, threshold);
304
+ if (prev.schema === 3) {
305
+ next.criterion_count = scenario.checklist.length;
306
+ next.rep_judgments = outcomes.map((outcome, repetition) => {
307
+ const fresh = next.rep_judgments?.find(panel => panel.repetition === repetition);
308
+ const prior = rec.rep_judgments?.find(panel => panel.repetition === repetition);
309
+ const judgments = fresh?.judgments.length ? fresh.judgments : prior?.judgments ?? [];
310
+ return {
311
+ repetition,
312
+ judgments,
313
+ recorded_verdict: outcome.verdict,
314
+ ...(outcome.objective ? { objective: outcome.objective } : {}),
315
+ };
316
+ });
317
+ }
287
318
  next.metrics = mergeScenarioMetrics(rec.metrics, next.metrics);
288
319
  // Overrides and their notes survive: a regate re-decides the gate, and an author
289
320
  // override is a statement about the judge, not about the needle.
290
- // `regate` re-evaluates gates from saved artifacts and asks no judge anything:
291
- // `objective` is freshly recomputed above, and the recorded judge panel still
292
- // describes the current judgments.
321
+ // `regate` re-evaluates gates from saved artifacts. It may judge a previously
322
+ // blocked fail→pass rep above; carrying the recorded panel still preserves any
323
+ // earlier multi-judge decision for judgments that were not replaced.
293
324
  scenarios.push(rebuildScenarioResult(next, rec, { objective: "fresh", adjudication: "carry" }));
294
325
  const to = next.judge_verdict;
295
326
  if (to !== rec.judge_verdict) {
@@ -302,6 +333,7 @@ export async function regateRun(opts) {
302
333
  }
303
334
  const ctx = scoreContextFor(prev, opts.spec);
304
335
  const results = writeResults(opts.runDir, {
336
+ schema: prev.schema, subject_invocations: prev.subject_invocations,
305
337
  skill: prev.skill, harness: prev.harness, model: prev.model,
306
338
  // Carried verbatim: a regate re-reads saved diffs, it never re-runs the harness.
307
339
  harness_cli_version: prev.harness_cli_version, delivery_canary: prev.delivery_canary,
package/dist/regrade.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Spec, Scenario } from "./spec.js";
2
2
  import type { HarnessAdapter, ModelRef } from "./adapters/types.js";
3
- import { type ScenarioResult, type ResultsFile } from "./results.js";
3
+ import { type Judgment, type ScenarioResult, type ResultsFile } from "./results.js";
4
4
  import { type RepOutcome } from "./reps.js";
5
5
  /**
6
6
  * Carry a run's recorded hashes forward, refreshing only the `rubric:` keys this
@@ -84,4 +84,6 @@ export interface RegradeRunOptions {
84
84
  * results.yaml, emits the `score` journal event, and returns the new
85
85
  * ResultsFile. Shared by `cmdGrade` and the pi-extension's `judge` command.
86
86
  */
87
+ /** Preserve prior panel votes and append a full-cell grade without writing invalid one-vote history. */
88
+ export declare function appendJudgeHistory(prior: ScenarioResult | undefined, priorJudge: ModelRef | undefined, fresh: Omit<Judgment, "ordinal">): Judgment[] | undefined;
87
89
  export declare function regradeRun(opts: RegradeRunOptions): Promise<ResultsFile>;
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, mergeScenarioMetrics, } from "./results.js";
4
+ import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, completeCriterionVotes, } 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";
@@ -63,6 +63,7 @@ export async function judgeOneRep(opts) {
63
63
  appendJournal(runDir, { event: "misfire-flag", ts: now(), id: scenario.id, reason: g.reason, ...repField });
64
64
  return {
65
65
  verdict: g.verdict, reason: g.reason, suspect: g.suspect,
66
+ judgment: { ordinal: 1, judge: { ...judge }, verdict: g.verdict, reason: g.reason, suspect: g.suspect, criteria: completeCriterionVotes(g.criteria, scenario.checklist.length) },
66
67
  metrics: {
67
68
  wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)),
68
69
  judge_calls: 1,
@@ -112,6 +113,22 @@ export async function regradeScenario(opts) {
112
113
  * results.yaml, emits the `score` journal event, and returns the new
113
114
  * ResultsFile. Shared by `cmdGrade` and the pi-extension's `judge` command.
114
115
  */
116
+ /** Preserve prior panel votes and append a full-cell grade without writing invalid one-vote history. */
117
+ export function appendJudgeHistory(prior, priorJudge, fresh) {
118
+ const history = prior?.judge_history ?? prior?.adjudication?.judgments ?? (prior && priorJudge ? [{
119
+ ordinal: 1,
120
+ judge: priorJudge,
121
+ verdict: prior.judge_verdict ?? "JUDGE-AMBIGUOUS",
122
+ reason: prior.judge_reason ?? "prior grade",
123
+ suspect: prior.suspect ?? true,
124
+ criteria: prior.rep_judgments?.find(panel => panel.repetition === 0)?.judgments[0]?.criteria ?? [],
125
+ }] : []);
126
+ const next = [
127
+ ...history,
128
+ { ...fresh, criteria: fresh.criteria ?? [], ordinal: history.length + 1 },
129
+ ].slice(-3).map((judgment, index) => ({ ...judgment, ordinal: index + 1 }));
130
+ return next.length >= 2 ? next : undefined;
131
+ }
115
132
  export async function regradeRun(opts) {
116
133
  const { runDir, spec, adapter, judge, specDir } = opts;
117
134
  const now = opts.now ?? (() => new Date().toISOString());
@@ -146,10 +163,16 @@ export async function regradeRun(opts) {
146
163
  targets = prev.scenarios
147
164
  .filter((s) => s.suspect || s.judge_verdict === "JUDGE-AMBIGUOUS")
148
165
  .map((s) => s.id);
149
- if (targets.length === 0) {
150
- // Nothing untrustworthy — a no-op, not an error. Return the file as-is.
151
- return prev;
152
- }
166
+ }
167
+ // Schema-3 delivery is a prerequisite for behavioral judging. A later `grade`
168
+ // cannot turn an undelivered or unauthenticated transcript into product evidence.
169
+ if (prev?.schema === 3) {
170
+ const blocked = new Set(prev.scenarios.filter(s => s.objective?.assertions.some(a => a.kind === "skill_delivered" && a.status !== "PASS")).map(s => s.id));
171
+ targets = targets.filter(id => !blocked.has(id));
172
+ }
173
+ if (targets.length === 0 && prev) {
174
+ // Nothing judgeable — a no-op, preserving both evidence and judge identity.
175
+ return prev;
153
176
  }
154
177
  const completeTranscripts = (record) => {
155
178
  const expected = record.reps ?? 1;
@@ -182,7 +205,14 @@ export async function regradeRun(opts) {
182
205
  expectedReps: prevScenario?.reps ?? 1,
183
206
  });
184
207
  const carry = overrides.get(id);
208
+ if (prev?.schema === 3)
209
+ rr.criterion_count = scenario.checklist.length;
185
210
  rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
211
+ rr.rep_judgments = carryRepObjectives(rr.rep_judgments, carry?.rep_judgments);
212
+ rr.judge_history = appendJudgeHistory(carry, prev?.judge, {
213
+ judge, verdict: rr.judge_verdict, reason: rr.judge_reason, suspect: rr.suspect,
214
+ criteria: rr.rep_judgments?.find(panel => panel.repetition === 0)?.judgments[0]?.criteria,
215
+ });
186
216
  // `grade` re-judges the saved transcript. It does not re-evaluate trace gates
187
217
  // (that is `regate`), so `objective` still describes this run; and it replaced
188
218
  // the judgments a prior adjudication described, so that panel must go.
@@ -190,6 +220,8 @@ export async function regradeRun(opts) {
190
220
  }
191
221
  const ctx = scoreContextFor({ mode, partial: prev?.partial }, spec);
192
222
  const results = writeResults(runDir, {
223
+ schema: prev?.schema,
224
+ subject_invocations: prev?.subject_invocations,
193
225
  skill: spec.skill,
194
226
  harness: prev?.harness ?? "pi",
195
227
  // The harness CLI that produced these transcripts, carried verbatim: a re-grade
package/dist/reps.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Verdict } from "./score.js";
2
- import type { ScenarioResult, ObjectiveResult } from "./results.js";
2
+ import type { ScenarioResult, ObjectiveResult, Judgment, SubjectInvocationObservation } from "./results.js";
3
3
  import type { TraceMetrics } from "./capture-trace-types.js";
4
4
  /** One rep's outcome (subject run + judge). */
5
5
  export interface RepOutcome {
@@ -8,6 +8,8 @@ export interface RepOutcome {
8
8
  suspect: boolean;
9
9
  /** Present only when the scenario declared structured objective assertions. */
10
10
  objective?: ObjectiveResult;
11
+ judgment?: Judgment;
12
+ subject_invocations?: SubjectInvocationObservation[];
11
13
  metrics?: {
12
14
  wall_time_ms: number;
13
15
  judge_calls: number;
@@ -22,7 +24,7 @@ export interface RepOutcome {
22
24
  * pass-threshold aggregation: an objective assertion is a statement about what
23
25
  * the model DID, so one rep that called a forbidden tool is a real finding, not
24
26
  * a minority draw to be voted away. ERROR dominates (missing evidence is never a
25
- * pass), then FAIL, then PASS. The retained assertion detail comes from the
27
+ * pass), then NOT-MEASURED (delivery absent), then FAIL, then PASS. The retained assertion detail comes from the
26
28
  * first non-passing rep, since that is the one worth reading.
27
29
  */
28
30
  export declare function aggregateObjective(outcomes: RepOutcome[]): ObjectiveResult | undefined;
package/dist/reps.js CHANGED
@@ -5,14 +5,14 @@
5
5
  * pass-threshold aggregation: an objective assertion is a statement about what
6
6
  * the model DID, so one rep that called a forbidden tool is a real finding, not
7
7
  * a minority draw to be voted away. ERROR dominates (missing evidence is never a
8
- * pass), then FAIL, then PASS. The retained assertion detail comes from the
8
+ * pass), then NOT-MEASURED (delivery absent), then FAIL, then PASS. The retained assertion detail comes from the
9
9
  * first non-passing rep, since that is the one worth reading.
10
10
  */
11
11
  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 picked = present.find((o) => o.status === "ERROR") ?? present.find((o) => o.status === "FAIL") ?? present[0];
15
+ const picked = present.find((o) => o.status === "ERROR") ?? present.find((o) => o.status === "NOT-MEASURED") ?? present.find((o) => o.status === "FAIL") ?? present[0];
16
16
  if (present.length === 1)
17
17
  return picked;
18
18
  const eventHashes = present.map((objective) => objective.events_sha256);
@@ -41,6 +41,10 @@ export function aggregateReps(outcomes, threshold) {
41
41
  if (errored > 0) {
42
42
  return { verdict: "ERROR", reason: `${errored}/${reps} reps errored — infrastructure, not behavior`, passes, reps, clean: clean.length, flakiness: 0, suspect: false };
43
43
  }
44
+ const notMeasured = outcomes.filter((o) => o.verdict === "NOT-MEASURED").length;
45
+ if (notMeasured > 0) {
46
+ return { verdict: "NOT-MEASURED", reason: `${notMeasured}/${reps} reps not measured — skill delivery was not established`, passes, reps, clean: clean.length - notMeasured, flakiness: 0, suspect: false };
47
+ }
44
48
  if (clean.length * 2 < reps) {
45
49
  // majority of reps misfired → untrustworthy
46
50
  return { verdict: "FAIL", reason: `${reps - clean.length}/${reps} reps misfired — re-judge`, passes, reps, clean: clean.length, flakiness: 0, suspect: true };
@@ -65,15 +69,17 @@ export function outcomesToResult(id, outcomes, repCount, threshold) {
65
69
  const objectiveField = objective ? { objective } : {};
66
70
  const metrics = aggregateMetrics(outcomes);
67
71
  const metricsField = metrics ? { metrics } : {};
72
+ const repJudgments = outcomes.map((outcome, repetition) => ({ repetition, judgments: outcome.judgment ? [outcome.judgment] : [], recorded_verdict: outcome.verdict, ...(outcome.objective ? { objective: outcome.objective } : {}) }));
73
+ const repJudgmentField = outcomes.some(outcome => outcome.judgment) ? { rep_judgments: repJudgments } : {};
68
74
  if (repCount === 1) {
69
75
  const o = outcomes[0];
70
- return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, ...metricsField, override: null, note: "", ...objectiveField };
76
+ return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, ...metricsField, override: null, note: "", ...objectiveField, ...repJudgmentField };
71
77
  }
72
78
  const agg = aggregateReps(outcomes, threshold);
73
79
  return {
74
80
  id, judge_verdict: agg.verdict, judge_reason: agg.reason, suspect: agg.suspect,
75
81
  reps: agg.reps, passes: agg.passes, clean: agg.clean, flakiness: agg.flakiness,
76
- pass_threshold: threshold, ...metricsField, override: null, note: "", ...objectiveField,
82
+ pass_threshold: threshold, ...metricsField, override: null, note: "", ...objectiveField, ...repJudgmentField,
77
83
  };
78
84
  }
79
85
  function aggregateMetrics(outcomes) {
@@ -97,6 +103,9 @@ function aggregateMetrics(outcomes) {
97
103
  cache_read_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_read_tokens, 0),
98
104
  cache_write_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_write_tokens, 0),
99
105
  subject_cost_usd: subjects.reduce((sum, metrics) => sum + metrics.cost_usd, 0),
106
+ cost_source: subjects.every((metrics) => metrics.cost_source === subjects[0].cost_source)
107
+ ? subjects[0].cost_source
108
+ : "unreported",
100
109
  tool_calls: subjects.reduce((sum, metrics) => sum + metrics.tool_calls, 0),
101
110
  delegated_children: subjects.reduce((sum, metrics) => sum + metrics.delegated_children, 0),
102
111
  max_concurrency: Math.max(...subjects.map((metrics) => metrics.max_concurrency)),
package/dist/rescore.js CHANGED
@@ -52,7 +52,7 @@ export function rescoreRun(opts) {
52
52
  // no rate to re-apply, or an untrustworthy verdict → carry verbatim
53
53
  if (!scenario || s.reps === undefined || s.clean === undefined || s.passes === undefined)
54
54
  return s;
55
- if (s.judge_verdict === "ERROR" || s.judge_verdict === "JUDGE-AMBIGUOUS")
55
+ if (s.judge_verdict === "ERROR" || s.judge_verdict === "NOT-MEASURED" || s.judge_verdict === "JUDGE-AMBIGUOUS")
56
56
  return s;
57
57
  if (s.clean === 0)
58
58
  return s;
@@ -78,6 +78,7 @@ export function rescoreRun(opts) {
78
78
  });
79
79
  const ctx = scoreContextFor(prev, opts.spec);
80
80
  const results = writeResults(opts.runDir, {
81
+ schema: prev.schema, subject_invocations: prev.subject_invocations,
81
82
  skill: prev.skill, harness: prev.harness, model: prev.model, judge: prev.judge,
82
83
  timestamp: prev.timestamp, label: prev.label, mode: prev.mode,
83
84
  partial: prev.partial,
package/dist/results.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type ModelRef } from "./adapters/types.js";
2
2
  import { type ScenarioVerdict } from "./score.js";
3
3
  import type { Verdict } from "./score.js";
4
4
  import type { ShipBar, Scenario } from "./spec.js";
5
+ import type { CostSource } from "./capture-trace-types.js";
5
6
  export interface ScenarioMetrics {
6
7
  wall_time_ms: number;
7
8
  judge_calls: number;
@@ -13,14 +14,36 @@ export interface ScenarioMetrics {
13
14
  cache_read_tokens?: number;
14
15
  cache_write_tokens?: number;
15
16
  subject_cost_usd?: number;
17
+ cost_source?: CostSource;
16
18
  tool_calls?: number;
17
19
  delegated_children?: number;
18
20
  max_concurrency?: number;
19
21
  }
20
22
  /** Merge bookkeeping from a later judge/gate pass without duplicating subject usage. */
21
23
  export declare function mergeScenarioMetrics(prior: ScenarioMetrics | undefined, fresh: ScenarioMetrics | undefined): ScenarioMetrics | undefined;
24
+ export interface CriterionVote {
25
+ index: number;
26
+ verdict: "PASS" | "FAIL" | "ERROR";
27
+ reason: string;
28
+ }
29
+ export interface RepJudgmentPanel {
30
+ repetition: number;
31
+ judgments: Judgment[];
32
+ recorded_verdict: Verdict;
33
+ /** Per-repetition objective result; needed because scenario.objective is a strict aggregate. */
34
+ objective?: ObjectiveResult;
35
+ }
36
+ export declare function carryRepObjectives(fresh: RepJudgmentPanel[] | undefined, prior: RepJudgmentPanel[] | undefined): RepJudgmentPanel[] | undefined;
37
+ export interface SubjectInvocationObservation {
38
+ scenario_id: string;
39
+ repetition: number;
40
+ attempt?: number;
41
+ prompt: import("./adapters/types.js").PromptProvenance;
42
+ }
22
43
  export interface ScenarioResult {
23
44
  id: string;
45
+ /** Numbered rubric items expected on every schema-3 judgment. */
46
+ criterion_count?: number;
24
47
  judge_verdict: Verdict;
25
48
  judge_reason: string;
26
49
  suspect: boolean;
@@ -49,6 +72,10 @@ export interface ScenarioResult {
49
72
  * actually blocks SHIP; this field is the audit trail behind that flag.
50
73
  */
51
74
  adjudication?: AdjudicationResult;
75
+ /** Latest three full-cell grades; enables offline agreement reports with distinct judges. */
76
+ judge_history?: Judgment[];
77
+ /** Initial and panel judge members, by repetition, sufficient to recompute the recorded verdict. */
78
+ rep_judgments?: RepJudgmentPanel[];
52
79
  }
53
80
  /** One judge's answer for a cell, kept verbatim however the collapse turned out. */
54
81
  export interface Judgment {
@@ -62,8 +89,12 @@ export interface Judgment {
62
89
  reason: string;
63
90
  /** The judge misfired — recorded, never counted as a clean vote. */
64
91
  suspect: boolean;
92
+ /** Every numbered rubric-item vote parsed from this exact judge response. Required in schema v3. */
93
+ criteria?: CriterionVote[];
65
94
  }
66
95
  export interface AdjudicationResult {
96
+ /** The saved transcript repetition judged by every panel member (first rep by policy). */
97
+ repetition?: number;
67
98
  state: "confirmed" | "tie_broken" | "unresolved";
68
99
  /** Why the cell was re-judged. */
69
100
  trigger: "ambiguous" | "contradictory" | "non_unanimous" | "ship_deciding";
@@ -74,8 +105,8 @@ export interface AdjudicationResult {
74
105
  }
75
106
  /** Objective gate outcome for one scenario cell. */
76
107
  export interface ObjectiveResult {
77
- /** ERROR means the evidence was missing or malformed — never a pass. */
78
- status: "PASS" | "FAIL" | "ERROR";
108
+ /** ERROR is malformed evidence; NOT-MEASURED is a known-undelivered experiment. */
109
+ status: "PASS" | "FAIL" | "ERROR" | "NOT-MEASURED";
79
110
  trace_version?: number;
80
111
  trace_sha256?: string;
81
112
  trajectory_version?: string;
@@ -85,7 +116,7 @@ export interface ObjectiveResult {
85
116
  rep_trace_sha256?: string[];
86
117
  assertions: {
87
118
  kind: string;
88
- status: "PASS" | "FAIL" | "ERROR";
119
+ status: "PASS" | "FAIL" | "ERROR" | "NOT-MEASURED";
89
120
  detail: string;
90
121
  }[];
91
122
  }
@@ -98,7 +129,7 @@ export interface GradeSummary {
98
129
  note: string;
99
130
  }
100
131
  export interface ResultsFile {
101
- schema: 2;
132
+ schema: 2 | 3;
102
133
  /**
103
134
  * The harness version that wrote this file — provenance for the numbers in it.
104
135
  *
@@ -177,8 +208,8 @@ export interface ResultsFile {
177
208
  * from 1 to 3 and re-running lands both runs in the same `+<arm>` tag with
178
209
  * byte-identical `arm` records, and `stability` reads a verdict flip between two
179
210
  * different conditions as one lineage being bimodal — the exact misreading the
180
- * tag placement exists to prevent. Recorded as DECLARED, with `<run-dir>`
181
- * unsubstituted: the substituted form differs every run for the same condition,
211
+ * tag placement exists to prevent. Recorded as DECLARED, with `<run-dir>` and
212
+ * `<workspace>` unsubstituted: the substituted form differs every run for the same condition,
182
213
  * which would make two identical conditions look like two.
183
214
  *
184
215
  * Optional only for records written before it was added; every writer emits it.
@@ -204,6 +235,8 @@ export interface ResultsFile {
204
235
  source_hashes?: Record<string, string>;
205
236
  effective_grade: GradeSummary;
206
237
  scenarios: ScenarioResult[];
238
+ /** Adapter-computed model-visible prompt observations. Required for schema v3. */
239
+ subject_invocations?: SubjectInvocationObservation[];
207
240
  }
208
241
  /**
209
242
  * The run modes whose results carry a real grade: the ones where the skill under
@@ -255,7 +288,9 @@ export declare function scoreContextFor(run: {
255
288
  /** The pass-threshold a re-grade uses: the run's persisted value, else the spec's per-scenario value, else 0.5. */
256
289
  export declare function effectiveThreshold(prevScenario: ScenarioResult | undefined, scenario: Scenario): number;
257
290
  /** Everything a caller may set. The grade is computed, never supplied. */
258
- export type ResultsDraft = Omit<ResultsFile, "schema" | "effective_grade">;
291
+ export type ResultsDraft = Omit<ResultsFile, "schema" | "effective_grade"> & {
292
+ schema?: 2 | 3;
293
+ };
259
294
  export interface ScoreContext {
260
295
  shipBar: ShipBar;
261
296
  critical: string[];
@@ -316,6 +351,17 @@ export declare function writeResults(runDir: string, draft: ResultsDraft, ctx: S
316
351
  export declare function migrateResults(raw: unknown): ResultsFile;
317
352
  /** Read results.yaml from a run dir, migrating schema-1 files in memory. */
318
353
  export declare function readResults(runDir: string): ResultsFile;
354
+ /** Parse rubric-item votes for retention only; verdict semantics remain in grade.ts. */
355
+ export declare function parseCriterionVotes(raw: string): CriterionVote[];
356
+ export declare function completeCriterionVotes(votes: CriterionVote[], expected: number): CriterionVote[];
357
+ export declare function deliveryStatusForObservations(observations: SubjectInvocationObservation[]): "PASS" | "NOT-MEASURED" | "ERROR";
358
+ export declare function recomputeRecordedPanels(results: ResultsFile): Array<{
359
+ scenario_id: string;
360
+ repetition: number;
361
+ verdict: Verdict;
362
+ }>;
363
+ /** Runtime v2/v3 validator. v2 remains permissive; v3 fails closed on observation gaps. */
364
+ export declare function validateResults(raw: unknown): ResultsFile;
319
365
  /** Pure: return a copy with override + note applied to one scenario. */
320
366
  export declare function applyOverride(results: ResultsFile, scenarioId: string, override: Verdict | null, note: string): ResultsFile;
321
367
  /**