@skill-harness/core 0.17.0 → 0.19.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 (95) hide show
  1. package/README.md +1 -2
  2. package/dist/adapters/types.d.ts +2 -12
  3. package/dist/capture-trace-types.d.ts +8 -102
  4. package/dist/capture-trace-types.js +0 -4
  5. package/dist/command-cost.d.ts +1 -1
  6. package/dist/command-cost.js +0 -5
  7. package/dist/execution-trace.d.ts +0 -16
  8. package/dist/execution-trace.js +31 -42
  9. package/dist/index.d.ts +3 -23
  10. package/dist/index.js +3 -23
  11. package/dist/instruction-coverage.d.ts +0 -7
  12. package/dist/instruction-coverage.js +5 -7
  13. package/dist/journal.d.ts +0 -8
  14. package/dist/metrics.d.ts +19 -0
  15. package/dist/metrics.js +28 -0
  16. package/dist/redaction.d.ts +6 -0
  17. package/dist/redaction.js +55 -0
  18. package/dist/regate.js +6 -71
  19. package/dist/regrade.d.ts +1 -3
  20. package/dist/regrade.js +1 -21
  21. package/dist/report.d.ts +1 -15
  22. package/dist/report.js +1 -13
  23. package/dist/reps.d.ts +1 -2
  24. package/dist/reps.js +33 -10
  25. package/dist/results.d.ts +10 -9
  26. package/dist/results.js +3 -19
  27. package/dist/run.js +15 -74
  28. package/dist/seeded.d.ts +1 -7
  29. package/dist/seeded.js +5 -11
  30. package/dist/sources.js +4 -9
  31. package/dist/spec.d.ts +0 -13
  32. package/dist/spec.js +6 -48
  33. package/dist/trajectory-events.d.ts +58 -0
  34. package/dist/trajectory-events.js +119 -0
  35. package/package.json +1 -1
  36. package/schemas/results-v2.schema.json +394 -75
  37. package/schemas/results-v3.schema.json +18 -0
  38. package/schemas/specification-v1.schema.json +128 -174
  39. package/dist/adjudication.d.ts +0 -219
  40. package/dist/adjudication.js +0 -422
  41. package/dist/adoption.d.ts +0 -128
  42. package/dist/adoption.js +0 -102
  43. package/dist/affected.d.ts +0 -88
  44. package/dist/affected.js +0 -222
  45. package/dist/capture.d.ts +0 -193
  46. package/dist/capture.js +0 -344
  47. package/dist/comparison.d.ts +0 -97
  48. package/dist/comparison.js +0 -189
  49. package/dist/factory-calibration.d.ts +0 -63
  50. package/dist/factory-calibration.js +0 -115
  51. package/dist/intervention-results.d.ts +0 -6
  52. package/dist/intervention-results.js +0 -52
  53. package/dist/intervention.d.ts +0 -123
  54. package/dist/intervention.js +0 -218
  55. package/dist/investigation.d.ts +0 -94
  56. package/dist/investigation.js +0 -148
  57. package/dist/judge-agreement.d.ts +0 -19
  58. package/dist/judge-agreement.js +0 -31
  59. package/dist/qualification-capture.d.ts +0 -44
  60. package/dist/qualification-capture.js +0 -163
  61. package/dist/qualification-config.d.ts +0 -233
  62. package/dist/qualification-config.js +0 -694
  63. package/dist/qualification-lock.d.ts +0 -15
  64. package/dist/qualification-lock.js +0 -87
  65. package/dist/qualification-oauth-directory.d.ts +0 -79
  66. package/dist/qualification-oauth-directory.js +0 -444
  67. package/dist/qualification-panel-store.d.ts +0 -14
  68. package/dist/qualification-panel-store.js +0 -188
  69. package/dist/qualification-panels.d.ts +0 -99
  70. package/dist/qualification-panels.js +0 -91
  71. package/dist/qualification-process.d.ts +0 -13
  72. package/dist/qualification-process.js +0 -190
  73. package/dist/qualification-runner.d.ts +0 -203
  74. package/dist/qualification-runner.js +0 -2037
  75. package/dist/qualification-store.d.ts +0 -187
  76. package/dist/qualification-store.js +0 -833
  77. package/dist/qualification-validation.d.ts +0 -12
  78. package/dist/qualification-validation.js +0 -8
  79. package/dist/screen.d.ts +0 -31
  80. package/dist/screen.js +0 -105
  81. package/dist/trajectory-gates.d.ts +0 -137
  82. package/dist/trajectory-gates.js +0 -576
  83. package/dist/trusted-host-supervision.d.ts +0 -25
  84. package/dist/trusted-host-supervision.js +0 -94
  85. package/dist/work-capture-schema.d.ts +0 -38
  86. package/dist/work-capture-schema.js +0 -33
  87. package/dist/work-capture.d.ts +0 -43
  88. package/dist/work-capture.js +0 -81
  89. package/dist/work-signals.d.ts +0 -55
  90. package/dist/work-signals.js +0 -71
  91. package/schemas/qualification-cell-result-v1.schema.json +0 -32
  92. package/schemas/qualification-config-v1.schema.json +0 -244
  93. package/schemas/qualification-invocation-request-v1.schema.json +0 -59
  94. package/schemas/qualification-judge-panel-result-v1.schema.json +0 -42
  95. package/schemas/trajectory-event-v1.schema.json +0 -80
@@ -0,0 +1,55 @@
1
+ /** Values longer than this are truncated rather than persisted whole. */
2
+ export const MAX_VALUE_CHARS = 2000;
3
+ const REDACTED = "[redacted]";
4
+ const SECRET_KEY = /^(.*[-_])?(password|passwd|secret|token|api[-_]?key|apikey|auth|authorization|credential|private[-_]?key|access[-_]?key|session[-_]?key)([-_].*)?$/i;
5
+ const SECRET_VALUE = [
6
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
7
+ /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/g,
8
+ /\bsk-[A-Za-z0-9]{16,}\b/g,
9
+ /\bgh[pousr]_[A-Za-z0-9]{16,}\b/g,
10
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g,
11
+ /\bAKIA[0-9A-Z]{16}\b/g,
12
+ /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
13
+ /\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi,
14
+ ];
15
+ export function redactText(input, homeDir) {
16
+ let out = input;
17
+ out = out.replace(SECRET_VALUE[SECRET_VALUE.length - 1], `$1${REDACTED}@`);
18
+ for (const re of SECRET_VALUE.slice(0, -1))
19
+ out = out.replace(re, REDACTED);
20
+ if (homeDir && homeDir.length > 1)
21
+ out = out.split(homeDir).join("~");
22
+ return out;
23
+ }
24
+ export function truncate(input, max = MAX_VALUE_CHARS) {
25
+ if (input.length <= max)
26
+ return input;
27
+ return `${input.slice(0, max)}… [truncated ${input.length - max} chars]`;
28
+ }
29
+ export function redactArgs(args, homeDir, depth = 0) {
30
+ if (args === null || typeof args !== "object" || Array.isArray(args))
31
+ return {};
32
+ const out = {};
33
+ for (const [key, value] of Object.entries(args)) {
34
+ if (SECRET_KEY.test(key)) {
35
+ out[key] = REDACTED;
36
+ continue;
37
+ }
38
+ out[key] = redactValue(value, homeDir, depth);
39
+ }
40
+ return out;
41
+ }
42
+ function redactValue(value, homeDir, depth) {
43
+ if (typeof value === "string")
44
+ return truncate(redactText(value, homeDir));
45
+ if (typeof value === "number" || typeof value === "boolean" || value === null)
46
+ return value;
47
+ if (depth >= 3)
48
+ return "[nested]";
49
+ if (Array.isArray(value))
50
+ return value.slice(0, 20).map((v) => redactValue(v, homeDir, depth + 1));
51
+ if (typeof value === "object")
52
+ return redactArgs(value, homeDir, depth + 1);
53
+ return String(value);
54
+ }
55
+ //# sourceMappingURL=redaction.js.map
package/dist/regate.js CHANGED
@@ -4,9 +4,8 @@ import { parseVerdict, detectMisfire } from "./grade.js";
4
4
  import { evaluateNeedleGates, hasNeedleGates } from "./seeded.js";
5
5
  import { evaluateTraceGates } from "./trace-gates.js";
6
6
  import { mergeTraces, deserializeTrace } from "./execution-trace.js";
7
- import { deserializeTrajectoryEvents, evaluateTrajectoryGates } from "./trajectory-gates.js";
8
7
  import { judgeOneRep } from "./regrade.js";
9
- import { readResults, writeResults, diffPath, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, findTrajectoryFiles, tracePath, trajectoryPath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, deliveryStatusForObservations, } from "./results.js";
8
+ import { readResults, writeResults, diffPath, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, tracePath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, deliveryStatusForObservations, } from "./results.js";
10
9
  import { outcomesToResult } from "./reps.js";
11
10
  import { appendJournal } from "./journal.js";
12
11
  import { gatesDigest, GATES_PREFIX } from "./sources.js";
@@ -89,8 +88,7 @@ export async function regateRun(opts) {
89
88
  const s = specById.get(rec.id);
90
89
  const needles = hasNeedleGates(s ?? {});
91
90
  const traceGated = Boolean(s?.traceAssert);
92
- const trajectoryGated = Boolean(s?.trajectoryAssert);
93
- if (!s || (!needles && !traceGated && !trajectoryGated))
91
+ if (!s || (!needles && !traceGated))
94
92
  continue; // nothing for regate to re-decide
95
93
  if (s.assert?.vitest || s.assert?.post_test) {
96
94
  blocked.push(`${s.id}: declares ${s.assert.vitest ? "assert.vitest" : "assert.post_test"}, which needs the workspace — ` +
@@ -112,16 +110,11 @@ export async function regateRun(opts) {
112
110
  `(traces are gitignored, or capture failed) — it needs a re-run`);
113
111
  continue;
114
112
  }
115
- if (trajectoryGated && !complete(findTrajectoryFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => trajectoryPath(opts.runDir, s.id, mode, rep)))) {
116
- blocked.push(`${s.id}: assert.trajectory artifacts are incomplete for ${expectedReps} recorded rep(s) ` +
117
- `(events are gitignored, or required native evidence was missing) — it needs a re-run`);
118
- continue;
119
- }
120
113
  targets.push(s);
121
114
  }
122
115
  if (targets.length === 0) {
123
116
  throw new Error(`nothing to regate in ${opts.runDir}` +
124
- (blocked.length > 0 ? `:\n ${blocked.join("\n ")}` : " — no scenario declares diff_contains/diff_excludes, assert.trace, or assert.trajectory"));
117
+ (blocked.length > 0 ? `:\n ${blocked.join("\n ")}` : " — no scenario declares diff_contains/diff_excludes or assert.trace"));
125
118
  }
126
119
  const changes = [];
127
120
  let judgeCalls = 0;
@@ -134,15 +127,9 @@ export async function regateRun(opts) {
134
127
  }
135
128
  const diffFiles = findDiffFiles(opts.runDir, scenario.id, mode);
136
129
  const traceFiles = findTraceFiles(opts.runDir, scenario.id, mode);
137
- const eventFiles = findTrajectoryFiles(opts.runDir, scenario.id, mode);
138
- // Reps come from whichever artifact this scenario actually has. A trajectory-only
139
- // scenario has neither diff nor trace, so falling back only once would regate
140
- // nothing and report success.
141
130
  const repKeys = diffFiles.length > 0
142
131
  ? diffFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: f }))
143
- : traceFiles.length > 0
144
- ? traceFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }))
145
- : eventFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }));
132
+ : traceFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }));
146
133
  const outcomes = [];
147
134
  // Per scenario, not run-wide: with several regated scenarios, a global counter
148
135
  // would report every change as "re-judged" because some other scenario was.
@@ -192,61 +179,9 @@ export async function regateRun(opts) {
192
179
  }
193
180
  }
194
181
  }
195
- // Adapter-neutral trajectory gate, replayed from the saved normalized
196
- // event artifact. Native ledgers are deliberately not re-read here: the
197
- // artifact is the immutable evidence this run actually captured.
198
- let trajectoryFailure = null;
199
- if (scenario.trajectoryAssert) {
200
- const ep = trajectoryPath(opts.runDir, scenario.id, mode, rep);
201
- const events = existsSync(ep) ? deserializeTrajectoryEvents(readFileSync(ep, "utf8")) : null;
202
- // The normalized artifact contains only events that were successfully captured.
203
- // Source-read/normalization failures are persisted on the original objective;
204
- // dropping them here would let a partial ledger become PASS on replay.
205
- const priorEvidenceErrors = (rec.objective?.assertions ?? [])
206
- .filter((result) => result.kind === "trajectory_evidence" && result.status === "ERROR");
207
- if (!events) {
208
- trajectoryFailure = "objective: saved normalized events are missing, malformed, or from an unsupported version";
209
- objective = {
210
- ...(objective ?? { assertions: [] }),
211
- status: "ERROR",
212
- assertions: [...(objective?.assertions ?? []), { kind: "trajectory_evidence", status: "ERROR", detail: trajectoryFailure }],
213
- };
214
- }
215
- else {
216
- const g = evaluateTrajectoryGates(scenario.trajectoryAssert, events);
217
- const expectedHash = rec.objective?.rep_events_sha256?.[rep ?? 0] ?? (rec.reps === undefined ? rec.objective?.events_sha256 : undefined);
218
- const digestMismatch = expectedHash !== undefined && expectedHash !== g.events_sha256;
219
- const integrityAssertion = digestMismatch
220
- ? [{ kind: "trajectory_evidence", status: "ERROR", detail: "saved normalized events no longer match the hash recorded by the run" }]
221
- : [];
222
- const priorStatus = objective?.status ?? "PASS";
223
- const status = priorStatus === "ERROR" || g.status === "ERROR" || priorEvidenceErrors.length || digestMismatch
224
- ? "ERROR"
225
- : priorStatus === "NOT-MEASURED"
226
- ? "NOT-MEASURED"
227
- : priorStatus === "FAIL" || g.status === "FAIL"
228
- ? "FAIL"
229
- : "PASS";
230
- objective = {
231
- ...(objective ?? {}),
232
- status,
233
- trajectory_version: g.event_version,
234
- events_sha256: g.events_sha256,
235
- assertions: [...(objective?.assertions ?? []), ...priorEvidenceErrors, ...integrityAssertion, ...g.assertions],
236
- };
237
- if (g.status !== "PASS" || priorEvidenceErrors.length || digestMismatch) {
238
- const details = [
239
- ...priorEvidenceErrors.map((result) => result.detail),
240
- ...integrityAssertion.map((result) => result.detail),
241
- ...g.assertions.filter((result) => result.status === g.status && g.status !== "PASS").map((result) => result.detail),
242
- ];
243
- trajectoryFailure = `objective: ${details.join("; ")}`;
244
- }
245
- }
246
- }
247
182
  // Prompt delivery was observed during the subject invocation and cannot be
248
183
  // replayed by regate. Carry its objective finding while refreshing only
249
- // trace/trajectory/needle assertions.
184
+ // trace/needle assertions.
250
185
  const retainedDelivery = rec.objective?.assertions.find(assertion => assertion.kind === "skill_delivered");
251
186
  const repObservations = prev.subject_invocations?.filter(observation => observation.scenario_id === scenario.id && observation.repetition === (rep ?? 0)) ?? [];
252
187
  const delivery = prev.schema === 3
@@ -259,7 +194,7 @@ export async function regateRun(opts) {
259
194
  if (delivery.status !== "PASS")
260
195
  traceFailure = `objective: ${delivery.detail}`;
261
196
  }
262
- const gate = { lines: needleGate.lines, failure: needleGate.failure ?? traceFailure ?? trajectoryFailure };
197
+ const gate = { lines: needleGate.lines, failure: needleGate.failure ?? traceFailure };
263
198
  const tPath = transcriptPath(opts.runDir, scenario.id, mode, rep);
264
199
  const before = existsSync(tPath) ? readFileSync(tPath, "utf8") : "";
265
200
  // Two sources, because the two gate kinds record their prior state
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 Judgment, type ScenarioResult, type ResultsFile } from "./results.js";
3
+ import { 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,6 +84,4 @@ 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;
89
87
  export declare function regradeRun(opts: RegradeRunOptions): Promise<ResultsFile>;
package/dist/regrade.js CHANGED
@@ -113,22 +113,6 @@ export async function regradeScenario(opts) {
113
113
  * results.yaml, emits the `score` journal event, and returns the new
114
114
  * ResultsFile. Shared by `cmdGrade` and the pi-extension's `judge` command.
115
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
- }
132
116
  export async function regradeRun(opts) {
133
117
  const { runDir, spec, adapter, judge, specDir } = opts;
134
118
  const now = opts.now ?? (() => new Date().toISOString());
@@ -140,7 +124,7 @@ export async function regradeRun(opts) {
140
124
  // silently downgraded a gated scenario to "no assertions declared".
141
125
  // `adjudication` is deliberately NOT carried: it describes the judgments this
142
126
  // re-grade just replaced, and a stale panel beside a fresh verdict is worse
143
- // than none. `grade --auto-rejudge` recomputes it.
127
+ // than none.
144
128
  // The whole prior result per id — `rebuildScenarioResult` decides, field by
145
129
  // field, what survives. Passing a hand-picked subset here is how fields got
146
130
  // dropped before.
@@ -209,10 +193,6 @@ export async function regradeRun(opts) {
209
193
  rr.criterion_count = scenario.checklist.length;
210
194
  rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
211
195
  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
- });
216
196
  // `grade` re-judges the saved transcript. It does not re-evaluate trace gates
217
197
  // (that is `regate`), so `objective` still describes this run; and it replaced
218
198
  // the judgments a prior adjudication described, so that panel must go.
package/dist/report.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type ShipBar } from "./spec.js";
2
2
  import { type ResultsFile } from "./results.js";
3
3
  import { type Lift } from "./lift.js";
4
- import { type AggregateMetrics } from "./comparison.js";
4
+ import { type AggregateMetrics } from "./metrics.js";
5
5
  export interface RunColumn {
6
6
  index: number;
7
7
  label: string;
@@ -22,13 +22,6 @@ export interface RunColumn {
22
22
  status: string;
23
23
  detail: string;
24
24
  };
25
- /** Adjudication outcome, when the cell was re-judged. */
26
- adjudication?: {
27
- state: string;
28
- trigger: string;
29
- count: number;
30
- detail: string;
31
- };
32
25
  reps?: number;
33
26
  passes?: number;
34
27
  clean?: number;
@@ -112,13 +105,6 @@ export declare function publicView(data: ReportData): {
112
105
  status: string;
113
106
  detail: string;
114
107
  };
115
- /** Adjudication outcome, when the cell was re-judged. */
116
- adjudication?: {
117
- state: string;
118
- trigger: string;
119
- count: number;
120
- detail: string;
121
- };
122
108
  reps?: number;
123
109
  passes?: number;
124
110
  clean?: number;
package/dist/report.js CHANGED
@@ -4,7 +4,7 @@ import { loadSpec } from "./spec.js";
4
4
  import { readResults } from "./results.js";
5
5
  import { collectLift, liftHeadline } from "./lift.js";
6
6
  import { boundaryCells, collectStability, stabilityNote } from "./stability.js";
7
- import { aggregateMetrics } from "./comparison.js";
7
+ import { aggregateMetrics } from "./metrics.js";
8
8
  /** Most-recent run dir (by name, which is an ISO-ish slug) under a model-tag dir. */
9
9
  function latestRunDir(tagDir) {
10
10
  if (!statSync(tagDir).isDirectory())
@@ -65,18 +65,6 @@ export function collectReport(skillDir) {
65
65
  },
66
66
  }
67
67
  : {}),
68
- ...(s.adjudication
69
- ? {
70
- adjudication: {
71
- state: s.adjudication.state,
72
- trigger: s.adjudication.trigger,
73
- count: s.adjudication.judgments.length,
74
- detail: s.adjudication.judgments
75
- .map((j) => `#${j.ordinal} ${j.judge.provider}:${j.judge.model} ${j.verdict}${j.suspect ? " (misfired, not counted)" : ""}`)
76
- .join(" · "),
77
- },
78
- }
79
- : {}),
80
68
  judge_verdict: s.judge_verdict,
81
69
  judge_reason: s.judge_reason,
82
70
  suspect: s.suspect ?? false, // suspect defaults false for older results that predate the field
package/dist/reps.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Verdict } from "./score.js";
2
- import type { ScenarioResult, ObjectiveResult, Judgment, SubjectInvocationObservation } from "./results.js";
2
+ import type { ScenarioResult, ObjectiveResult, Judgment } 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 {
@@ -9,7 +9,6 @@ export interface RepOutcome {
9
9
  /** Present only when the scenario declared structured objective assertions. */
10
10
  objective?: ObjectiveResult;
11
11
  judgment?: Judgment;
12
- subject_invocations?: SubjectInvocationObservation[];
13
12
  metrics?: {
14
13
  wall_time_ms: number;
15
14
  judge_calls: number;
package/dist/reps.js CHANGED
@@ -15,11 +15,9 @@ export function aggregateObjective(outcomes) {
15
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
- const eventHashes = present.map((objective) => objective.events_sha256);
19
18
  const traceHashes = present.map((objective) => objective.trace_sha256);
20
19
  return {
21
20
  ...picked,
22
- ...(eventHashes.every((hash) => typeof hash === "string") ? { rep_events_sha256: eventHashes } : {}),
23
21
  ...(traceHashes.every((hash) => typeof hash === "string") ? { rep_trace_sha256: traceHashes } : {}),
24
22
  };
25
23
  }
@@ -69,17 +67,32 @@ export function outcomesToResult(id, outcomes, repCount, threshold) {
69
67
  const objectiveField = objective ? { objective } : {};
70
68
  const metrics = aggregateMetrics(outcomes);
71
69
  const metricsField = metrics ? { metrics } : {};
70
+ const usage = outcomes.flatMap((outcome, repetition) => {
71
+ const subject = outcome.metrics?.subject;
72
+ if (!subject)
73
+ return [];
74
+ const reported = (value) => value !== null && value > 0 ? value : null;
75
+ return [{
76
+ repetition,
77
+ inputTokens: reported(subject.input_tokens),
78
+ outputTokens: reported(subject.output_tokens),
79
+ cacheReadTokens: reported(subject.cache_read_tokens),
80
+ costUsd: reported(subject.cost_usd),
81
+ priceAsOf: subject.price_as_of,
82
+ }];
83
+ });
84
+ const usageField = usage.length ? { usage } : {};
72
85
  const repJudgments = outcomes.map((outcome, repetition) => ({ repetition, judgments: outcome.judgment ? [outcome.judgment] : [], recorded_verdict: outcome.verdict, ...(outcome.objective ? { objective: outcome.objective } : {}) }));
73
86
  const repJudgmentField = outcomes.some(outcome => outcome.judgment) ? { rep_judgments: repJudgments } : {};
74
87
  if (repCount === 1) {
75
88
  const o = outcomes[0];
76
- return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, ...metricsField, override: null, note: "", ...objectiveField, ...repJudgmentField };
89
+ return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, ...metricsField, ...usageField, override: null, note: "", ...objectiveField, ...repJudgmentField };
77
90
  }
78
91
  const agg = aggregateReps(outcomes, threshold);
79
92
  return {
80
93
  id, judge_verdict: agg.verdict, judge_reason: agg.reason, suspect: agg.suspect,
81
94
  reps: agg.reps, passes: agg.passes, clean: agg.clean, flakiness: agg.flakiness,
82
- pass_threshold: threshold, ...metricsField, override: null, note: "", ...objectiveField, ...repJudgmentField,
95
+ pass_threshold: threshold, ...metricsField, ...usageField, override: null, note: "", ...objectiveField, ...repJudgmentField,
83
96
  };
84
97
  }
85
98
  function aggregateMetrics(outcomes) {
@@ -87,22 +100,32 @@ function aggregateMetrics(outcomes) {
87
100
  if (present.length === 0)
88
101
  return undefined;
89
102
  const subjects = present.map((metrics) => metrics.subject).filter((metrics) => metrics !== undefined);
103
+ const reportedSubjects = subjects.filter(metrics => metrics.input_tokens !== null || metrics.output_tokens !== null || metrics.cache_read_tokens !== null);
90
104
  const base = {
91
105
  wall_time_ms: present.reduce((sum, metrics) => sum + metrics.wall_time_ms, 0),
92
106
  judge_calls: present.reduce((sum, metrics) => sum + metrics.judge_calls, 0),
93
107
  judge_rejudge_calls: present.reduce((sum, metrics) => sum + metrics.judge_rejudge_calls, 0),
94
- subject_metrics_reps: subjects.length,
108
+ subject_metrics_reps: reportedSubjects.length,
95
109
  total_reps: outcomes.length,
96
110
  };
97
111
  if (subjects.length === 0)
98
112
  return base;
113
+ const sumReported = (field) => {
114
+ const values = subjects.map(metrics => metrics[field]).filter((value) => value !== null && value > 0);
115
+ return values.length ? values.reduce((sum, value) => sum + value, 0) : undefined;
116
+ };
117
+ const inputTokens = sumReported("input_tokens");
118
+ const outputTokens = sumReported("output_tokens");
119
+ const cacheReadTokens = sumReported("cache_read_tokens");
120
+ const cacheWriteTokens = sumReported("cache_write_tokens");
121
+ const subjectCost = sumReported("cost_usd");
99
122
  return {
100
123
  ...base,
101
- input_tokens: subjects.reduce((sum, metrics) => sum + metrics.input_tokens, 0),
102
- output_tokens: subjects.reduce((sum, metrics) => sum + metrics.output_tokens, 0),
103
- cache_read_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_read_tokens, 0),
104
- cache_write_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_write_tokens, 0),
105
- subject_cost_usd: subjects.reduce((sum, metrics) => sum + metrics.cost_usd, 0),
124
+ ...(inputTokens === undefined ? {} : { input_tokens: inputTokens }),
125
+ ...(outputTokens === undefined ? {} : { output_tokens: outputTokens }),
126
+ ...(cacheReadTokens === undefined ? {} : { cache_read_tokens: cacheReadTokens }),
127
+ ...(cacheWriteTokens === undefined ? {} : { cache_write_tokens: cacheWriteTokens }),
128
+ ...(subjectCost === undefined ? {} : { subject_cost_usd: subjectCost }),
106
129
  cost_source: subjects.every((metrics) => metrics.cost_source === subjects[0].cost_source)
107
130
  ? subjects[0].cost_source
108
131
  : "unreported",
package/dist/results.d.ts CHANGED
@@ -3,6 +3,14 @@ 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
+ export interface SubjectUsage {
7
+ repetition: number;
8
+ inputTokens: number | null;
9
+ outputTokens: number | null;
10
+ cacheReadTokens: number | null;
11
+ costUsd: number | null;
12
+ priceAsOf: string | null;
13
+ }
6
14
  export interface ScenarioMetrics {
7
15
  wall_time_ms: number;
8
16
  judge_calls: number;
@@ -56,6 +64,8 @@ export interface ScenarioResult {
56
64
  pass_threshold?: number;
57
65
  /** Cost/latency counters. Separate from verdicts: cheaper failure is still failure. */
58
66
  metrics?: ScenarioMetrics;
67
+ /** Per-repetition subject usage; null values mean Pi did not report that counter. */
68
+ usage?: SubjectUsage[];
59
69
  /**
60
70
  * Objective trace-gate evidence. ADDITIVE and optional.
61
71
  *
@@ -72,8 +82,6 @@ export interface ScenarioResult {
72
82
  * actually blocks SHIP; this field is the audit trail behind that flag.
73
83
  */
74
84
  adjudication?: AdjudicationResult;
75
- /** Latest three full-cell grades; enables offline agreement reports with distinct judges. */
76
- judge_history?: Judgment[];
77
85
  /** Initial and panel judge members, by repetition, sufficient to recompute the recorded verdict. */
78
86
  rep_judgments?: RepJudgmentPanel[];
79
87
  }
@@ -109,10 +117,7 @@ export interface ObjectiveResult {
109
117
  status: "PASS" | "FAIL" | "ERROR" | "NOT-MEASURED";
110
118
  trace_version?: number;
111
119
  trace_sha256?: string;
112
- trajectory_version?: string;
113
- events_sha256?: string;
114
120
  /** Per-repetition hashes retained when an aggregate has more than one rep. */
115
- rep_events_sha256?: string[];
116
121
  rep_trace_sha256?: string[];
117
122
  assertions: {
118
123
  kind: string;
@@ -443,10 +448,6 @@ export declare function rebuildScenarioResult(fresh: ScenarioResult, prior: Scen
443
448
  * traces append without a wrapper.
444
449
  */
445
450
  export declare function tracePath(runDir: string, scenarioId: string, mode: string, rep?: number): string;
446
- /** Saved adapter-neutral workflow events for one scenario rep. */
447
- export declare function trajectoryPath(runDir: string, scenarioId: string, mode: string, rep?: number): string;
448
- /** A scenario's normalized-event files, sorted (plain first, then numeric rep). */
449
- export declare function findTrajectoryFiles(runDir: string, scenarioId: string, mode?: string): string[];
450
451
  /** A scenario's staged-diff files, sorted (plain first, then numeric rep). Mode-scoped when given. */
451
452
  export declare function findDiffFiles(runDir: string, scenarioId: string, mode?: string): string[];
452
453
  /** A scenario's execution-trace files, sorted (plain first, then numeric rep). Mode-scoped when given. */
package/dist/results.js CHANGED
@@ -409,7 +409,7 @@ export function validateResults(raw) {
409
409
  const aggregateObjectiveStatus = objectiveStatuses.includes("ERROR") ? "ERROR" : objectiveStatuses.includes("NOT-MEASURED") ? "NOT-MEASURED" : objectiveStatuses.includes("FAIL") ? "FAIL" : "PASS";
410
410
  if (!scenario.objective || scenario.objective.status !== aggregateObjectiveStatus)
411
411
  throw new Error(`scenario objective diverges from per-repetition objectives for ${scenario.id}`);
412
- for (const judgment of [...(scenario.judge_history ?? []), ...(scenario.adjudication?.judgments ?? [])])
412
+ for (const judgment of scenario.adjudication?.judgments ?? [])
413
413
  assertCriteria(judgment);
414
414
  let adjudicatedVerdict;
415
415
  if (scenario.adjudication) {
@@ -599,7 +599,7 @@ export function diffPath(runDir, scenarioId, mode, rep) {
599
599
  export function rebuildScenarioResult(fresh, prior, policy) {
600
600
  // Exhaustive destructure. Do not replace with a spread: the spread is what
601
601
  // allowed a new field to pass through unconsidered in the first place.
602
- const { id, criterion_count: freshCriterionCount, judge_verdict, judge_reason, suspect, override: _freshOverride, note: _freshNote, reps, passes, clean, flakiness, pass_threshold, metrics: freshMetrics, objective: freshObjective, adjudication: freshAdjudication, judge_history: freshJudgeHistory, rep_judgments: freshRepJudgments, ...rest } = fresh;
602
+ const { id, criterion_count: freshCriterionCount, judge_verdict, judge_reason, suspect, override: _freshOverride, note: _freshNote, reps, passes, clean, flakiness, pass_threshold, metrics: freshMetrics, usage: freshUsage, objective: freshObjective, adjudication: freshAdjudication, rep_judgments: freshRepJudgments, ...rest } = fresh;
603
603
  const _exhaustive = rest;
604
604
  void _exhaustive;
605
605
  void _freshOverride;
@@ -652,6 +652,7 @@ export function rebuildScenarioResult(fresh, prior, policy) {
652
652
  ...(flakiness === undefined ? {} : { flakiness }),
653
653
  ...(pass_threshold === undefined ? {} : { pass_threshold }),
654
654
  ...((freshMetrics ?? prior?.metrics) ? { metrics: freshMetrics ?? prior.metrics } : {}),
655
+ ...((freshUsage ?? prior?.usage) ? { usage: freshUsage ?? prior.usage } : {}),
655
656
  // The author owns the verdict; a re-measurement never discards their call.
656
657
  override: prior?.override ?? null,
657
658
  note: prior?.note ?? "",
@@ -660,7 +661,6 @@ export function rebuildScenarioResult(fresh, prior, policy) {
660
661
  // existed.
661
662
  ...(objective ? { objective } : {}),
662
663
  ...(adjudication ? { adjudication } : {}),
663
- ...((freshJudgeHistory ?? prior?.judge_history) ? { judge_history: freshJudgeHistory ?? prior.judge_history } : {}),
664
664
  ...((freshRepJudgments ?? prior?.rep_judgments) ? { rep_judgments: freshRepJudgments ?? prior.rep_judgments } : {}),
665
665
  };
666
666
  }
@@ -675,21 +675,6 @@ export function tracePath(runDir, scenarioId, mode, rep) {
675
675
  const base = rep === undefined ? `${scenarioId}.${mode}` : `${scenarioId}.${mode}.rep${rep}`;
676
676
  return join(runDir, `${base}.trace.jsonl`);
677
677
  }
678
- /** Saved adapter-neutral workflow events for one scenario rep. */
679
- export function trajectoryPath(runDir, scenarioId, mode, rep) {
680
- const base = rep === undefined ? `${scenarioId}.${mode}` : `${scenarioId}.${mode}.rep${rep}`;
681
- return join(runDir, `${base}.events.jsonl`);
682
- }
683
- /** A scenario's normalized-event files, sorted (plain first, then numeric rep). */
684
- export function findTrajectoryFiles(runDir, scenarioId, mode) {
685
- if (!existsSync(runDir))
686
- return [];
687
- const esc = scenarioId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
688
- const re = mode === undefined
689
- ? new RegExp(`^${esc}\\..*\\.events\\.jsonl$`)
690
- : new RegExp(`^${esc}\\.${mode}(\\.rep\\d+)?\\.events\\.jsonl$`);
691
- return sortByRep(readdirSync(runDir).filter((f) => re.test(f)));
692
- }
693
678
  /** A scenario's staged-diff files, sorted (plain first, then numeric rep). Mode-scoped when given. */
694
679
  export function findDiffFiles(runDir, scenarioId, mode) {
695
680
  if (!existsSync(runDir))
@@ -738,7 +723,6 @@ export function preserveTranscript(resultsRoot, runDir, scenarioId) {
738
723
  // override whose justification was gitignored, and left `regate` with nothing
739
724
  // to re-evaluate on the one cell a human had disputed.
740
725
  ...findTraceFiles(runDir, scenarioId),
741
- ...findTrajectoryFiles(runDir, scenarioId),
742
726
  ];
743
727
  if (files.length === 0)
744
728
  return;