@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
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @skill-harness/core
2
2
 
3
3
  The engine behind [skill-harness](https://github.com/mojomanyana/skill-harness):
4
- spec parsing, discovery, run orchestration, LLM-judge grading, scoring, results.
4
+ spec parsing, discovery, run orchestration, LLM-judge grading, scoring, results,
5
+ and the separately versioned durable qualification-runner-v1 primitives.
5
6
 
6
7
  Internal API — use the `skill-harness` CLI. See the repo for docs.
7
8
 
@@ -11,6 +11,21 @@ export interface ModelRef {
11
11
  export declare function parseModelRef(token: string): ModelRef;
12
12
  /** Stable, filesystem-safe slug for a model ref (used in run-dir names). */
13
13
  export declare function modelSlug(ref: ModelRef): string;
14
+ export type PromptMechanism = "none" | "pi-skill" | "append-system-prompt" | "system-prompt-file";
15
+ export interface PromptProvenance {
16
+ capture_version: "prompt-provenance-v1";
17
+ request_index: number;
18
+ raw_sha256: string;
19
+ normalized_sha256: string;
20
+ normalization_rule: "cwd-line-v1";
21
+ bytes: number;
22
+ contract_sha256: string;
23
+ contract_bytes: number;
24
+ contract_occurrences: number;
25
+ mechanism: PromptMechanism;
26
+ status: "PASS" | "NOT-MEASURED" | "ERROR";
27
+ error?: string;
28
+ }
14
29
  export interface RunReq {
15
30
  skillDir: string;
16
31
  model: ModelRef;
@@ -50,6 +65,8 @@ export interface RunReq {
50
65
  * inheriting a caller's copy of them.
51
66
  */
52
67
  armEnv?: Record<string, string>;
68
+ /** Adapter-produced observation callback. Callers receive evidence; they never supply delivery status. */
69
+ onPromptObservation?: (observation: PromptProvenance) => void;
53
70
  }
54
71
  /** A judge request: single prompt, no skills, no session. */
55
72
  export interface JudgeReq {
@@ -74,6 +91,8 @@ export interface StructuredRun {
74
91
  }
75
92
  export interface HarnessAdapter {
76
93
  name: string;
94
+ /** True only when the adapter computes final provider-payload observations. */
95
+ observesPrompts?: boolean;
77
96
  available(): Promise<boolean>;
78
97
  run(req: RunReq): Promise<string>;
79
98
  /**
@@ -1,5 +1,5 @@
1
1
  import type { HarnessAdapter, ModelRef } from "./adapters/types.js";
2
- import type { ScenarioResult, AdjudicationResult, Judgment, ResultsFile } from "./results.js";
2
+ import type { ScenarioResult, AdjudicationResult, Judgment, ResultsFile, CriterionVote } from "./results.js";
3
3
  import type { Scenario, ShipBar, Spec } from "./spec.js";
4
4
  import type { Verdict } from "./score.js";
5
5
  /**
@@ -33,6 +33,11 @@ export interface CellState {
33
33
  suspect: boolean;
34
34
  /** Per-rep verdicts, when the cell ran with reps. Empty for a single-rep cell. */
35
35
  repVerdicts?: Verdict[];
36
+ criteria?: CriterionVote[];
37
+ /** Delivery prerequisite for schema-3 cells; non-PASS cells are never judged. */
38
+ deliveryStatus?: "PASS" | "NOT-MEASURED" | "ERROR";
39
+ /** The actual selected repetition's primary vote; aggregate fields remain for planning only. */
40
+ primaryJudgment?: Judgment;
36
41
  }
37
42
  export interface TriggerDecision {
38
43
  id: string;
@@ -105,11 +110,14 @@ export declare function collapseJudgments(judgments: Judgment[], trigger: Trigge
105
110
  */
106
111
  export declare function boundAdjudicationToRepetitions(result: ScenarioResult, scenario: Scenario, adj: AdjudicationResult): AdjudicationResult;
107
112
  export declare function projectAdjudication(result: ScenarioResult, adj: AdjudicationResult): ScenarioResult;
113
+ /** Apply the one-transcript bound and label it only when the bound changed the panel result. */
114
+ export declare function projectAdjudicationForScenario(result: ScenarioResult, scenario: Scenario, adj: AdjudicationResult): ScenarioResult;
108
115
  /** Ask one judge about one cell's saved transcript. Supplied by the caller. */
109
116
  export type RejudgeFn = (id: string, judge: ModelRef) => Promise<{
110
117
  verdict: Verdict;
111
118
  reason: string;
112
119
  suspect: boolean;
120
+ criteria?: CriterionVote[];
113
121
  }>;
114
122
  export interface RunAdjudicationOptions {
115
123
  plan: AdjudicationPlan;
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { judgeRawPath, writeResults, scoreContextFor, findTranscriptFiles, repIndexOf, rebuildScenarioResult, mergeScenarioMetrics, effectiveThreshold, } from "./results.js";
3
+ import { judgeRawPath, writeResults, scoreContextFor, findTranscriptFiles, repIndexOf, rebuildScenarioResult, mergeScenarioMetrics, effectiveThreshold, completeCriterionVotes, } from "./results.js";
4
4
  import { score } from "./score.js";
5
5
  import { buildJudgePrompt, judgeInWorkspace, parseVerdict } from "./grade.js";
6
6
  import { appendJournal } from "./journal.js";
7
+ import { collapseVotePanel } from "./vote-panel.js";
7
8
  /** Maximum judgments per cell, first wave included. Hard cap, not a default. */
8
9
  export const MAX_JUDGMENTS = 3;
9
10
  /**
@@ -18,6 +19,9 @@ export function planAdjudication(input) {
18
19
  const enabled = new Set(input.enabled ?? ["ambiguous", "contradictory", "non_unanimous", "ship_deciding"]);
19
20
  const decisions = [];
20
21
  for (const cell of input.cells) {
22
+ // Delivery-gated schema-3 cells carry no behavioral claim to adjudicate.
23
+ if (cell.deliveryStatus && cell.deliveryStatus !== "PASS")
24
+ continue;
21
25
  const triggers = [];
22
26
  // `JUDGE-AMBIGUOUS` is what the parser emits when a judge's verdict blocks
23
27
  // disagree. `ERROR` is what it emits when nothing parsed at all — and that
@@ -92,24 +96,13 @@ function flipsShipDecision(cell, input) {
92
96
  * cast a deciding vote would launder exactly the unreliability being measured.
93
97
  */
94
98
  export function collapseJudgments(judgments, trigger) {
95
- const clean = judgments.filter((j) => !j.suspect && (j.verdict === "PASS" || j.verdict === "FAIL"));
96
- const base = { trigger, judgments };
97
- if (clean.length < 2) {
98
- // Nothing to compare: one clean vote (or none) cannot confirm anything.
99
- return { ...base, state: "unresolved" };
100
- }
101
- const passes = clean.filter((j) => j.verdict === "PASS").length;
102
- const fails = clean.length - passes;
103
- if (passes === 0 || fails === 0) {
104
- return { ...base, state: "confirmed", verdict: clean[0].verdict };
105
- }
106
- // A clean strict majority breaks the tie. With exactly 2 clean votes that
107
- // disagree there is no majority, so this correctly falls through.
108
- if (passes > fails)
109
- return { ...base, state: "tie_broken", verdict: "PASS" };
110
- if (fails > passes)
111
- return { ...base, state: "tie_broken", verdict: "FAIL" };
112
- return { ...base, state: "unresolved" };
99
+ const collapsed = collapseVotePanel(judgments);
100
+ return {
101
+ trigger,
102
+ judgments,
103
+ state: collapsed.state,
104
+ ...(collapsed.verdict ? { verdict: collapsed.verdict } : {}),
105
+ };
113
106
  }
114
107
  /**
115
108
  * Project an adjudication onto the compatibility fields the scorer reads.
@@ -149,6 +142,15 @@ export function projectAdjudication(result, adj) {
149
142
  adjudication: adj,
150
143
  };
151
144
  }
145
+ /** Apply the one-transcript bound and label it only when the bound changed the panel result. */
146
+ export function projectAdjudicationForScenario(result, scenario, adj) {
147
+ const bounded = boundAdjudicationToRepetitions(result, scenario, adj);
148
+ const projected = projectAdjudication(result, bounded);
149
+ if (bounded !== adj) {
150
+ projected.judge_reason = `${adj.judgments.length} judgments on one transcript cannot replace a critical all-repetitions aggregate`;
151
+ }
152
+ return projected;
153
+ }
152
154
  function reasonFor(adj) {
153
155
  const n = adj.judgments.length;
154
156
  const verb = adj.state === "confirmed" ? "confirmed by" : "resolved by majority of";
@@ -179,7 +181,9 @@ export async function runAdjudication(opts) {
179
181
  // in the fixed order `planAdjudication` evaluates them.
180
182
  const trigger = decision.triggers[0];
181
183
  const judgments = [
182
- { ordinal: 1, judge: { ...opts.primaryJudge }, verdict: cell.verdict, reason: cell.reason, suspect: cell.suspect },
184
+ cell.primaryJudgment
185
+ ? { ...cell.primaryJudgment, ordinal: 1 }
186
+ : { ordinal: 1, judge: { ...opts.primaryJudge }, verdict: cell.verdict, reason: cell.reason, suspect: cell.suspect, criteria: cell.criteria ?? [] },
183
187
  ];
184
188
  const second = await opts.rejudge(decision.id, opts.secondaryJudge);
185
189
  callsMade++;
@@ -242,11 +246,8 @@ export async function adjudicateRun(opts) {
242
246
  if (!adj)
243
247
  return s;
244
248
  const scenario = byIdScenario.get(s.id);
245
- const boundedAdj = scenario ? boundAdjudicationToRepetitions(s, scenario, adj) : adj;
246
- const projected = projectAdjudication(s, boundedAdj);
247
- if (boundedAdj !== adj) {
248
- projected.judge_reason = `${adj.judgments.length} judgments on one transcript cannot replace a critical all-repetitions aggregate`;
249
- }
249
+ const withRepetition = { ...adj, repetition: s.rep_judgments?.[0]?.repetition ?? 0 };
250
+ const projected = scenario ? projectAdjudicationForScenario(s, scenario, withRepetition) : projectAdjudication(s, withRepetition);
250
251
  const extraCalls = adj.judgments.filter((judgment) => judgment.ordinal > 1).length;
251
252
  projected.metrics = mergeScenarioMetrics(s.metrics, {
252
253
  wall_time_ms: 0,
@@ -299,7 +300,7 @@ async function judgeCell(opts) {
299
300
  event: "judge-verdict", ts: opts.now(), id: opts.scenario.id,
300
301
  verdict: g.verdict, reason: g.reason, suspect: g.suspect,
301
302
  });
302
- return { verdict: g.verdict, reason: g.reason, suspect: g.suspect };
303
+ return { verdict: g.verdict, reason: g.reason, suspect: g.suspect, criteria: completeCriterionVotes(g.criteria, opts.scenario.checklist.length) };
303
304
  }
304
305
  /**
305
306
  * Build the plan's input cells from a run.
@@ -311,13 +312,20 @@ async function judgeCell(opts) {
311
312
  * about — inverting the one invariant this feature actually promises.
312
313
  */
313
314
  export function cellsFromResults(runDir, results) {
314
- return results.scenarios.map((s) => ({
315
- id: s.id,
316
- verdict: s.judge_verdict,
317
- reason: s.judge_reason,
318
- suspect: s.suspect,
319
- repVerdicts: repVerdictsOf(runDir, s, results.mode),
320
- }));
315
+ return results.scenarios.map((s) => {
316
+ const primaryJudgment = s.rep_judgments?.find(panel => panel.repetition === 0)?.judgments[0];
317
+ const deliveryStatus = s.objective?.assertions.find(assertion => assertion.kind === "skill_delivered")?.status;
318
+ return {
319
+ id: s.id,
320
+ verdict: s.judge_verdict,
321
+ reason: s.judge_reason,
322
+ suspect: s.suspect,
323
+ repVerdicts: repVerdictsOf(runDir, s, results.mode),
324
+ criteria: primaryJudgment?.criteria,
325
+ ...(deliveryStatus === "PASS" || deliveryStatus === "NOT-MEASURED" || deliveryStatus === "ERROR" ? { deliveryStatus } : {}),
326
+ ...(primaryJudgment ? { primaryJudgment } : {}),
327
+ };
328
+ });
321
329
  }
322
330
  /** Per-rep verdicts from saved judge artifacts, for the non-unanimous trigger. */
323
331
  function repVerdictsOf(runDir, s, mode) {
package/dist/arms.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface Arm {
18
18
  seedSkills: string[];
19
19
  /** Minimum definitions the seeding must produce, else the run ERRORs. */
20
20
  requireDefinitions: number;
21
- /** Env for the subject process. `<run-dir>` is substituted per run. */
21
+ /** Env for the subject process. `<run-dir>` and `<workspace>` are substituted per rep. */
22
22
  env: Record<string, string>;
23
23
  }
24
24
  /** The implicit control: today's behaviour, byte-identical, no extensions. */
@@ -83,12 +83,14 @@ export interface TraceResultMeta {
83
83
  * conversation carries in the session dir, not in the event stream). This
84
84
  * mirrors the loop the adapter already runs.
85
85
  */
86
+ export type CostSource = "provider-reported" | "subscription" | "unreported";
86
87
  export interface TraceMetrics {
87
88
  input_tokens: number;
88
89
  output_tokens: number;
89
90
  cache_read_tokens: number;
90
91
  cache_write_tokens: number;
91
92
  cost_usd: number;
93
+ cost_source: CostSource;
92
94
  tool_calls: number;
93
95
  delegated_children: number;
94
96
  /** Maximum simultaneously outstanding tool calls observed in the pi stream. */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * CLI commands guaranteed to make no subject or judge calls.
3
+ *
4
+ * Keep this list narrow: commands that can conditionally spend (notably `regate`)
5
+ * do not belong here even when their common path only replays saved artifacts.
6
+ */
7
+ export declare const FREE_OFFLINE_COMMANDS: readonly ["affected", "coverage", "init", "judge-agreement", "lint", "list", "mutation-test", "rescore", "restamp", "screen", "stability"];
8
+ export type FreeOfflineCommand = typeof FREE_OFFLINE_COMMANDS[number];
9
+ export declare function isFreeOfflineCommand(command: string): command is FreeOfflineCommand;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CLI commands guaranteed to make no subject or judge calls.
3
+ *
4
+ * Keep this list narrow: commands that can conditionally spend (notably `regate`)
5
+ * do not belong here even when their common path only replays saved artifacts.
6
+ */
7
+ export const FREE_OFFLINE_COMMANDS = [
8
+ "affected",
9
+ "coverage",
10
+ "init",
11
+ "judge-agreement",
12
+ "lint",
13
+ "list",
14
+ "mutation-test",
15
+ "rescore",
16
+ "restamp",
17
+ "screen",
18
+ "stability",
19
+ ];
20
+ export function isFreeOfflineCommand(command) {
21
+ return FREE_OFFLINE_COMMANDS.some((candidate) => candidate === command);
22
+ }
23
+ //# sourceMappingURL=command-cost.js.map
@@ -45,6 +45,7 @@ export interface AggregateMetrics {
45
45
  cache_read_tokens: number | null;
46
46
  cache_write_tokens: number | null;
47
47
  subject_cost_usd: number | null;
48
+ cost_source: "provider-reported" | "subscription" | "unreported" | null;
48
49
  tool_calls: number | null;
49
50
  delegated_children: number | null;
50
51
  max_concurrency: number | null;
@@ -51,8 +51,8 @@ export function buildComparison(input) {
51
51
  const behavioral = cells.filter((cell) => cell.change === "regression").map((cell) => cell.id);
52
52
  const criticalRegressions = cells.filter((cell) => cell.change === "regression" && cell.critical).map((cell) => cell.id);
53
53
  const byReference = new Map(reference.scenarios.map((scenario) => [scenario.id, scenario]));
54
- const infrastructureErrors = cells.filter((cell) => cell.reference === "ERROR" || cell.reference === "JUDGE-AMBIGUOUS" || byReference.get(cell.id)?.suspect ||
55
- cell.candidate === "ERROR" || cell.candidate === "JUDGE-AMBIGUOUS" || byCandidate.get(cell.id)?.suspect).map((cell) => cell.id);
54
+ const infrastructureErrors = cells.filter((cell) => cell.reference === "ERROR" || cell.reference === "NOT-MEASURED" || cell.reference === "JUDGE-AMBIGUOUS" || byReference.get(cell.id)?.suspect ||
55
+ cell.candidate === "ERROR" || cell.candidate === "NOT-MEASURED" || cell.candidate === "JUDGE-AMBIGUOUS" || byCandidate.get(cell.id)?.suspect).map((cell) => cell.id);
56
56
  const metrics = { reference: aggregateMetrics(reference.scenarios), candidate: aggregateMetrics(candidate.scenarios) };
57
57
  const costRegressions = thresholdFindings(metrics.reference, metrics.candidate, input.thresholds);
58
58
  const partial = input.partial === true || reference.partial === true || candidate.partial === true;
@@ -137,6 +137,10 @@ export function aggregateMetrics(scenarios) {
137
137
  cache_read_tokens: sumOptional("cache_read_tokens"),
138
138
  cache_write_tokens: sumOptional("cache_write_tokens"),
139
139
  subject_cost_usd: sumOptional("subject_cost_usd"),
140
+ cost_source: (() => {
141
+ const sources = scenarios.map((scenario) => scenario.metrics?.cost_source).filter((source) => source !== undefined);
142
+ return sources.length === 0 ? null : sources.every((source) => source === sources[0]) ? sources[0] : "unreported";
143
+ })(),
140
144
  tool_calls: sumOptional("tool_calls"),
141
145
  delegated_children: sumOptional("delegated_children"),
142
146
  max_concurrency: maxValues.length ? Math.max(...maxValues) : null,
@@ -147,11 +151,13 @@ function effective(result) {
147
151
  return result.override;
148
152
  if (result.objective?.status === "ERROR")
149
153
  return "ERROR";
154
+ if (result.objective?.status === "NOT-MEASURED")
155
+ return "NOT-MEASURED";
150
156
  if (result.objective?.status === "FAIL")
151
157
  return "FAIL";
152
158
  return result.judge_verdict;
153
159
  }
154
- function untrustworthy(result, verdict) { return result.suspect || verdict === "ERROR" || verdict === "JUDGE-AMBIGUOUS"; }
160
+ function untrustworthy(result, verdict) { return result.suspect || verdict === "ERROR" || verdict === "NOT-MEASURED" || verdict === "JUDGE-AMBIGUOUS"; }
155
161
  function sorted(value) { return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))); }
156
162
  function increase(reference, candidate) { return reference === null || candidate === null || reference <= 0 ? null : (candidate - reference) / reference; }
157
163
  function thresholdFindings(reference, candidate, thresholds) {
@@ -157,12 +157,19 @@ export function parseTrace(lines, meta) {
157
157
  }
158
158
  }
159
159
  const toolCalls = [...calls.values()].sort((a, b) => a.issueIndex - b.issueIndex);
160
+ const subscription = meta.subject.provider === "openai-codex" || meta.subject.provider === "claude-code";
161
+ const costSource = subscription
162
+ ? "subscription"
163
+ : cost !== null
164
+ ? "provider-reported"
165
+ : "unreported";
160
166
  const metrics = {
161
167
  input_tokens: inputTokens,
162
168
  output_tokens: outputTokens,
163
169
  cache_read_tokens: cacheReadTokens,
164
170
  cache_write_tokens: cacheWriteTokens,
165
171
  cost_usd: cost ?? 0,
172
+ cost_source: costSource,
166
173
  tool_calls: toolCalls.length,
167
174
  delegated_children: toolCalls
168
175
  .filter((call) => call.name === "Agent")
@@ -348,12 +355,13 @@ export function mergeTraces(traces) {
348
355
  cache_read_tokens: sum.cache_read_tokens + trace.metrics.cache_read_tokens,
349
356
  cache_write_tokens: sum.cache_write_tokens + trace.metrics.cache_write_tokens,
350
357
  cost_usd: sum.cost_usd + trace.metrics.cost_usd,
358
+ cost_source: sum.cost_source === trace.metrics.cost_source ? sum.cost_source : "unreported",
351
359
  tool_calls: sum.tool_calls + trace.metrics.tool_calls,
352
360
  delegated_children: sum.delegated_children + trace.metrics.delegated_children,
353
361
  max_concurrency: Math.max(sum.max_concurrency, trace.metrics.max_concurrency),
354
362
  }), {
355
363
  input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0,
356
- cost_usd: 0, tool_calls: 0, delegated_children: 0, max_concurrency: 0,
364
+ cost_usd: 0, cost_source: traces[0].metrics.cost_source, tool_calls: 0, delegated_children: 0, max_concurrency: 0,
357
365
  })
358
366
  : undefined;
359
367
  const last = traces[traces.length - 1];
package/dist/grade.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Scenario } from "./spec.js";
2
2
  import type { HarnessAdapter, ModelRef } from "./adapters/types.js";
3
3
  import type { Verdict } from "./score.js";
4
+ import { type CriterionVote } from "./results.js";
4
5
  export interface JudgePromptInput {
5
6
  skill: string;
6
7
  persona: string;
@@ -36,6 +37,7 @@ export declare function parseVerdict(out: string): ParsedVerdict;
36
37
  export declare function judgeResemblesSubject(judge: ModelRef, subject: ModelRef): boolean;
37
38
  export interface GradeResult extends ParsedVerdict {
38
39
  raw: string;
40
+ criteria: CriterionVote[];
39
41
  /** Judge misfire: the overall verdict disagrees with AND(per-item grades). Recorded, never auto-passed; blocks SHIP until re-judged or overridden. */
40
42
  suspect: boolean;
41
43
  }
package/dist/grade.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createWorkspace } from "./workspace.js";
2
+ import { parseCriterionVotes } from "./results.js";
2
3
  /** The heading runSeeded writes above the staged diff. Gating on this exact string is what keeps the guidance honest — see below. */
3
4
  export const STAGED_DIFF_HEADING = "=== STAGED DIFF ===";
4
5
  /**
@@ -133,7 +134,7 @@ export async function gradeTranscript(adapter, judge, prompt, cwd) {
133
134
  parsed.reason = `judge unparseable: ${snippet}`;
134
135
  }
135
136
  const suspect = detectMisfire(raw, parsed.verdict);
136
- return { ...parsed, raw, suspect };
137
+ return { ...parsed, raw, suspect, criteria: parseCriterionVotes(raw) };
137
138
  }
138
139
  /**
139
140
  * Grade a transcript in a fresh, isolated, throwaway workspace — never the
package/dist/index.d.ts CHANGED
@@ -23,8 +23,12 @@ export * from "./util/env.js";
23
23
  export * from "./scaffold.js";
24
24
  export * from "./version.js";
25
25
  export * from "./defaults.js";
26
+ export * from "./command-cost.js";
26
27
  export * from "./judge-policy.js";
28
+ export * from "./judge-agreement.js";
27
29
  export * from "./regate.js";
30
+ export * from "./screen.js";
31
+ export * from "./prompt-normalization.js";
28
32
  export * from "./downgrade.js";
29
33
  export * from "./canary.js";
30
34
  export * from "./stability.js";
@@ -37,7 +41,15 @@ export * from "./trajectory-gates.js";
37
41
  export * from "./instruction-coverage.js";
38
42
  export * from "./affected.js";
39
43
  export * from "./adjudication.js";
44
+ export * from "./vote-panel.js";
45
+ export * from "./qualification-panels.js";
46
+ export * from "./qualification-panel-store.js";
47
+ export * from "./qualification-validation.js";
40
48
  export * from "./comparison.js";
41
49
  export * from "./sandbox.js";
42
50
  export * from "./provider-failure.js";
43
51
  export * from "./arms.js";
52
+ export * from "./qualification-config.js";
53
+ export * from "./qualification-oauth-directory.js";
54
+ export * from "./qualification-store.js";
55
+ export * from "./qualification-runner.js";
package/dist/index.js CHANGED
@@ -23,8 +23,12 @@ export * from "./util/env.js";
23
23
  export * from "./scaffold.js";
24
24
  export * from "./version.js";
25
25
  export * from "./defaults.js";
26
+ export * from "./command-cost.js";
26
27
  export * from "./judge-policy.js";
28
+ export * from "./judge-agreement.js";
27
29
  export * from "./regate.js";
30
+ export * from "./screen.js";
31
+ export * from "./prompt-normalization.js";
28
32
  export * from "./downgrade.js";
29
33
  export * from "./canary.js";
30
34
  export * from "./stability.js";
@@ -37,8 +41,16 @@ export * from "./trajectory-gates.js";
37
41
  export * from "./instruction-coverage.js";
38
42
  export * from "./affected.js";
39
43
  export * from "./adjudication.js";
44
+ export * from "./vote-panel.js";
45
+ export * from "./qualification-panels.js";
46
+ export * from "./qualification-panel-store.js";
47
+ export * from "./qualification-validation.js";
40
48
  export * from "./comparison.js";
41
49
  export * from "./sandbox.js";
42
50
  export * from "./provider-failure.js";
43
51
  export * from "./arms.js";
52
+ export * from "./qualification-config.js";
53
+ export * from "./qualification-oauth-directory.js";
54
+ export * from "./qualification-store.js";
55
+ export * from "./qualification-runner.js";
44
56
  //# sourceMappingURL=index.js.map
package/dist/journal.d.ts CHANGED
@@ -103,7 +103,7 @@ export type JournalEvent = {
103
103
  }
104
104
  /**
105
105
  * A regate: needle gates re-evaluated against the saved staged diffs. `judge_calls`
106
- * is on the record because regate is advertised as free apart from the reps whose
106
+ * is on the record because regate may spend only on the reps whose
107
107
  * gate verdict flipped — a claim the journal should be able to settle. `skipped`
108
108
  * names scenarios it could not regate (vitest/post_test, or missing diff artifacts).
109
109
  */
@@ -0,0 +1,19 @@
1
+ import type { ResultsFile } from "./results.js";
2
+ import type { Verdict } from "./score.js";
3
+ export interface JudgeAgreementCell {
4
+ scenario: string;
5
+ status: "agree" | "disagree" | "error";
6
+ judges: string[];
7
+ verdicts: Verdict[];
8
+ detail: string;
9
+ }
10
+ export interface JudgeAgreementReport {
11
+ cells: JudgeAgreementCell[];
12
+ agree: number;
13
+ disagree: number;
14
+ error: number;
15
+ comparable: number;
16
+ rate: number | null;
17
+ }
18
+ /** Pure/offline projection of judgments already persisted in results.yaml. */
19
+ export declare function judgeAgreement(results: ResultsFile): JudgeAgreementReport;
@@ -0,0 +1,31 @@
1
+ /** Pure/offline projection of judgments already persisted in results.yaml. */
2
+ export function judgeAgreement(results) {
3
+ const cells = results.scenarios.map((scenario) => {
4
+ const judgments = scenario.judge_history ?? scenario.adjudication?.judgments ?? [];
5
+ const distinct = new Map();
6
+ for (const judgment of judgments) {
7
+ const key = `${judgment.judge.provider}:${judgment.judge.model}`;
8
+ distinct.delete(key); // insertion order now reflects each judge's latest persisted vote
9
+ distinct.set(key, judgment);
10
+ }
11
+ const pair = [...distinct.entries()].slice(-2);
12
+ if (pair.length < 2 || pair.some(([, judgment]) => judgment.suspect || judgment.verdict === "ERROR" || judgment.verdict === "NOT-MEASURED" || judgment.verdict === "JUDGE-AMBIGUOUS")) {
13
+ return {
14
+ scenario: scenario.id, status: "error", judges: pair.map(([judge]) => judge),
15
+ verdicts: pair.map(([, judgment]) => judgment.verdict),
16
+ detail: pair.length < 2 ? "fewer than two distinct persisted judges" : "one or more judgments is not a clean vote",
17
+ };
18
+ }
19
+ const status = pair[0][1].verdict === pair[1][1].verdict ? "agree" : "disagree";
20
+ return {
21
+ scenario: scenario.id, status, judges: pair.map(([judge]) => judge),
22
+ verdicts: pair.map(([, judgment]) => judgment.verdict), detail: `${pair[0][1].verdict} vs ${pair[1][1].verdict}`,
23
+ };
24
+ });
25
+ const agree = cells.filter((cell) => cell.status === "agree").length;
26
+ const disagree = cells.filter((cell) => cell.status === "disagree").length;
27
+ const error = cells.filter((cell) => cell.status === "error").length;
28
+ const comparable = agree + disagree;
29
+ return { cells, agree, disagree, error, comparable, rate: comparable ? agree / comparable : null };
30
+ }
31
+ //# sourceMappingURL=judge-agreement.js.map
@@ -4,15 +4,17 @@ import { BAKED_DEFAULT_JUDGE } from "./defaults.js";
4
4
  * Judge providers that cannot bill a per-token API.
5
5
  *
6
6
  * `claude-code` shells out to the `claude` CLI, authenticated with the user's
7
- * Claude subscription (OAuth). `ollama`/`lmstudio`/`llamacpp`/`local` are local
8
- * runtimes. Everything else is assumed to charge.
7
+ * Claude subscription (OAuth). `openai-codex` stays in Pi and authenticates with
8
+ * the user's ChatGPT subscription (OAuth). `ollama`/`lmstudio`/`llamacpp`/`local`
9
+ * are local runtimes. Everything else — including direct `openai` — is assumed
10
+ * to charge.
9
11
  *
10
12
  * An **allow-list**, deliberately, not a deny-list of known-paid providers: a
11
13
  * provider nobody has classified yet should be treated as able to bill, because
12
14
  * being wrong in that direction produces a surprise invoice while being wrong in
13
15
  * this direction produces one extra flag.
14
16
  */
15
- const FREE_JUDGE_PROVIDERS = new Set(["claude-code", "ollama", "lmstudio", "llamacpp", "local"]);
17
+ const FREE_JUDGE_PROVIDERS = new Set(["claude-code", "openai-codex", "ollama", "lmstudio", "llamacpp", "local"]);
16
18
  /** Whether judging with this ref can charge a per-token API. */
17
19
  export function isMeteredJudge(judge) {
18
20
  return !FREE_JUDGE_PROVIDERS.has(judge.provider);
package/dist/lift.js CHANGED
@@ -29,7 +29,7 @@ function conclusive(verdict, suspect) {
29
29
  // misfire are judge failures. Treating any of them as a FAIL would make
30
30
  // "red FAIL -> green PASS" fire on infrastructure noise, and lift would
31
31
  // measure flakiness while looking like skill value.
32
- return !suspect && verdict !== "ERROR" && verdict !== "JUDGE-AMBIGUOUS";
32
+ return !suspect && verdict !== "ERROR" && verdict !== "NOT-MEASURED" && verdict !== "JUDGE-AMBIGUOUS";
33
33
  }
34
34
  function classify(red, green) {
35
35
  if (!conclusive(red.verdict, red.suspect) || !conclusive(green.verdict, green.suspect))
package/dist/lint.js CHANGED
@@ -279,7 +279,7 @@ export function lintSkill(skillDir) {
279
279
  else if (current !== recorded) {
280
280
  // The remedy is per key kind, and it is the whole point of the split: a
281
281
  // rubric edit is re-gradeable from saved transcripts, a policy edit is a
282
- // free rescore, a needle edit is a free regate. Only stimulus drift costs
282
+ // free rescore, while a gate edit avoids a subject re-run via regate. Only stimulus drift costs
283
283
  // model spend. One message saying "re-run" for all four is what made
284
284
  // correcting a known-bad rubric expensive enough to skip.
285
285
  findings.push({ skill, scenario, code: "stale", message: `${what} changed since the newest ${basename(tagDir)} run (${newest}) — results are stale; ${remedyForKey(key)}` });
@@ -0,0 +1,6 @@
1
+ export declare const PROMPT_NORMALIZATION_RULE: "cwd-line-v1";
2
+ export declare const PROMPT_NORMALIZATION_PATTERN = "^(Current working directory:)[^\\r\\n]*(\\r?)$";
3
+ export declare const PROMPT_NORMALIZATION_FLAGS = "gm";
4
+ export declare const PROMPT_NORMALIZATION_REPLACEMENT = "$1<normalized>$2";
5
+ export declare const PROMPT_NORMALIZATION_SOURCE_KEY = "observation:prompt-normalization";
6
+ export declare const PROMPT_NORMALIZATION_SOURCE_DIGEST: string;
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ export const PROMPT_NORMALIZATION_RULE = "cwd-line-v1";
3
+ export const PROMPT_NORMALIZATION_PATTERN = "^(Current working directory:)[^\\r\\n]*(\\r?)$";
4
+ export const PROMPT_NORMALIZATION_FLAGS = "gm";
5
+ export const PROMPT_NORMALIZATION_REPLACEMENT = "$1<normalized>$2";
6
+ export const PROMPT_NORMALIZATION_SOURCE_KEY = "observation:prompt-normalization";
7
+ export const PROMPT_NORMALIZATION_SOURCE_DIGEST = createHash("sha256").update(JSON.stringify([
8
+ "prompt-normalization-registry",
9
+ PROMPT_NORMALIZATION_RULE,
10
+ PROMPT_NORMALIZATION_PATTERN,
11
+ PROMPT_NORMALIZATION_FLAGS,
12
+ PROMPT_NORMALIZATION_REPLACEMENT,
13
+ ])).digest("hex");
14
+ //# sourceMappingURL=prompt-normalization.js.map
@@ -0,0 +1,44 @@
1
+ export interface QualificationOutputReceipt {
2
+ path: string;
3
+ total_bytes: number;
4
+ captured_bytes: number;
5
+ truncated: boolean;
6
+ sha256: string;
7
+ }
8
+ export interface QualificationBoundedCapture {
9
+ path: string;
10
+ total_bytes: number;
11
+ captured_bytes: number;
12
+ truncated: boolean;
13
+ write(chunk: Buffer): void;
14
+ close(): void;
15
+ }
16
+ export interface QualificationArtifactAttestation {
17
+ ok: boolean;
18
+ actual: {
19
+ provider: string;
20
+ model: string;
21
+ } | null;
22
+ fallback: boolean;
23
+ refused: boolean;
24
+ error: string | null;
25
+ }
26
+ export declare function redactQualificationOutput(text: string, secretValues?: readonly string[]): string;
27
+ export declare function openQualificationBoundedCapture(path: string, limit: number, redact: (text: string) => string): QualificationBoundedCapture;
28
+ export declare function qualificationOutputReceipt(spoolRoot: string, path: string, capture: QualificationBoundedCapture): QualificationOutputReceipt;
29
+ export declare function verifyQualificationOutputReceipt(spoolRoot: string, receipt: QualificationOutputReceipt): void;
30
+ export declare function attestQualificationPiJsonl(text: string, requested: {
31
+ provider: string;
32
+ model: string;
33
+ }): QualificationArtifactAttestation;
34
+ export declare function spawnQualificationCapture(command: string, argv: string[], options: {
35
+ cwd: string;
36
+ env: NodeJS.ProcessEnv;
37
+ timeout_ms: number;
38
+ output_limit_bytes: number;
39
+ }): Promise<{
40
+ stdout: string;
41
+ stderr: string;
42
+ code: number | null;
43
+ timed_out: boolean;
44
+ }>;