@skill-harness/core 0.11.0 → 0.13.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 +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/adoption.d.ts +128 -0
  6. package/dist/adoption.js +102 -0
  7. package/dist/arms.d.ts +1 -1
  8. package/dist/capture-trace-types.d.ts +2 -0
  9. package/dist/command-cost.d.ts +9 -0
  10. package/dist/command-cost.js +23 -0
  11. package/dist/comparison.d.ts +1 -0
  12. package/dist/comparison.js +9 -3
  13. package/dist/execution-trace.js +9 -1
  14. package/dist/factory-calibration.d.ts +63 -0
  15. package/dist/factory-calibration.js +115 -0
  16. package/dist/grade.d.ts +2 -0
  17. package/dist/grade.js +2 -1
  18. package/dist/index.d.ts +21 -0
  19. package/dist/index.js +21 -0
  20. package/dist/intervention-results.d.ts +6 -0
  21. package/dist/intervention-results.js +52 -0
  22. package/dist/intervention.d.ts +123 -0
  23. package/dist/intervention.js +218 -0
  24. package/dist/investigation.d.ts +94 -0
  25. package/dist/investigation.js +148 -0
  26. package/dist/journal.d.ts +1 -1
  27. package/dist/judge-agreement.d.ts +19 -0
  28. package/dist/judge-agreement.js +31 -0
  29. package/dist/judge-policy.js +5 -3
  30. package/dist/lift.js +1 -1
  31. package/dist/lint.js +1 -1
  32. package/dist/prompt-normalization.d.ts +6 -0
  33. package/dist/prompt-normalization.js +14 -0
  34. package/dist/qualification-capture.d.ts +44 -0
  35. package/dist/qualification-capture.js +163 -0
  36. package/dist/qualification-config.d.ts +233 -0
  37. package/dist/qualification-config.js +694 -0
  38. package/dist/qualification-lock.d.ts +15 -0
  39. package/dist/qualification-lock.js +87 -0
  40. package/dist/qualification-oauth-directory.d.ts +79 -0
  41. package/dist/qualification-oauth-directory.js +444 -0
  42. package/dist/qualification-panel-store.d.ts +14 -0
  43. package/dist/qualification-panel-store.js +188 -0
  44. package/dist/qualification-panels.d.ts +99 -0
  45. package/dist/qualification-panels.js +91 -0
  46. package/dist/qualification-process.d.ts +13 -0
  47. package/dist/qualification-process.js +190 -0
  48. package/dist/qualification-runner.d.ts +203 -0
  49. package/dist/qualification-runner.js +2037 -0
  50. package/dist/qualification-store.d.ts +187 -0
  51. package/dist/qualification-store.js +833 -0
  52. package/dist/qualification-validation.d.ts +12 -0
  53. package/dist/qualification-validation.js +8 -0
  54. package/dist/regate.js +42 -10
  55. package/dist/regrade.d.ts +3 -1
  56. package/dist/regrade.js +37 -5
  57. package/dist/reps.d.ts +4 -2
  58. package/dist/reps.js +13 -4
  59. package/dist/rescore.js +2 -1
  60. package/dist/results.d.ts +53 -7
  61. package/dist/results.js +210 -7
  62. package/dist/run.js +66 -17
  63. package/dist/score.d.ts +4 -3
  64. package/dist/score.js +13 -4
  65. package/dist/screen.d.ts +31 -0
  66. package/dist/screen.js +105 -0
  67. package/dist/seeded.d.ts +2 -1
  68. package/dist/seeded.js +1 -0
  69. package/dist/sources.d.ts +7 -1
  70. package/dist/sources.js +13 -5
  71. package/dist/spec.d.ts +1 -1
  72. package/dist/spec.js +1 -1
  73. package/dist/stability.js +1 -1
  74. package/dist/trajectory-gates.d.ts +14 -14
  75. package/dist/trajectory-gates.js +18 -34
  76. package/dist/trusted-host-supervision.d.ts +25 -0
  77. package/dist/trusted-host-supervision.js +94 -0
  78. package/dist/vote-panel.d.ts +33 -0
  79. package/dist/vote-panel.js +41 -0
  80. package/dist/work-capture-schema.d.ts +38 -0
  81. package/dist/work-capture-schema.js +33 -0
  82. package/dist/work-capture.d.ts +43 -0
  83. package/dist/work-capture.js +81 -0
  84. package/dist/work-signals.d.ts +55 -0
  85. package/dist/work-signals.js +71 -0
  86. package/dist/workspace.js +4 -1
  87. package/package.json +3 -3
  88. package/schemas/qualification-cell-result-v1.schema.json +32 -0
  89. package/schemas/qualification-config-v1.schema.json +244 -0
  90. package/schemas/qualification-invocation-request-v1.schema.json +59 -0
  91. package/schemas/qualification-judge-panel-result-v1.schema.json +42 -0
  92. package/schemas/results-v2.schema.json +19 -21
  93. package/schemas/results-v3.schema.json +654 -0
  94. package/schemas/specification-v1.schema.json +1 -1
  95. package/schemas/trajectory-event-v1.schema.json +20 -1
@@ -0,0 +1,218 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ const SHA = /^[a-f0-9]{64}$/;
3
+ const text = (x, max = 512) => typeof x === "string" && x.length > 0 && x.length <= max && !/[\u0000-\u001f\u007f]/.test(x);
4
+ const closed = (x, names) => !!x && typeof x === "object" && !Array.isArray(x) && Object.keys(x).sort().join() === [...names].sort().join();
5
+ function canonical(x, depth = 0) {
6
+ if (depth > 16)
7
+ throw new Error("intervention data exceeds depth");
8
+ if (x === null || typeof x === "string" || typeof x === "boolean" || (typeof x === "number" && Number.isFinite(x)))
9
+ return JSON.stringify(x);
10
+ if (!x || typeof x !== "object" || (!Array.isArray(x) && ![Object.prototype, null].includes(Object.getPrototypeOf(x))))
11
+ throw new Error("plain intervention JSON required");
12
+ const d = Object.getOwnPropertyDescriptors(x);
13
+ if (Array.isArray(x)) {
14
+ if (x.length > 4096 || Reflect.ownKeys(x).length !== x.length + 1)
15
+ throw new Error("bounded dense array required");
16
+ return `[${Array.from({ length: x.length }, (_, i) => { if (!d[i] || !Object.hasOwn(d[i], "value"))
17
+ throw new Error("plain array required"); return canonical(d[i].value, depth + 1); }).join(",")}]`;
18
+ }
19
+ return `{${Reflect.ownKeys(d).sort((a, b) => String(a) < String(b) ? -1 : 1).map(k => { if (typeof k !== "string" || !d[k].enumerable || !Object.hasOwn(d[k], "value"))
20
+ throw new Error("plain object required"); return `${JSON.stringify(k)}:${canonical(d[k].value, depth + 1)}`; }).join(",")}}`;
21
+ }
22
+ /** Plain, bounded JSON encoding for retained intervention inputs; not semantic validation. */
23
+ export function interventionCanonicalJson(value) {
24
+ const bytes = canonical(value);
25
+ if (Buffer.byteLength(bytes) > 2 * 1024 * 1024)
26
+ throw new Error("intervention data exceeds byte bound");
27
+ return bytes;
28
+ }
29
+ const digest = (value) => createHash("sha256").update(interventionCanonicalJson(value)).digest("hex");
30
+ function frozen(value) { if (value && typeof value === "object") {
31
+ Object.values(value).forEach(frozen);
32
+ Object.freeze(value);
33
+ } return value; }
34
+ const clone = (value) => JSON.parse(canonical(value));
35
+ const axes = ["model", "effort", "skill", "prompt", "configuration"];
36
+ export function freezeIntervention(input) {
37
+ const d = clone(input);
38
+ if (!closed(d, ["family", "investigationSha256", "resourceMetric", "axes", "common", "proposer", "judge", "cases", "arms"]) || d.family !== "intervention" || !SHA.test(d.investigationSha256)
39
+ || !["usd", "wall_ms", "tool_calls"].includes(d.resourceMetric)
40
+ || !Array.isArray(d.axes) || !d.axes.length || new Set(d.axes).size !== d.axes.length || d.axes.some(a => !axes.includes(a))
41
+ || !closed(d.common, ["mode", "scenarioSha256", "rubricSha256", "fixtureSha256", "heldoutSha256", "harnessSha256", "judgePolicySha256"])
42
+ || !["green", "force"].includes(d.common.mode) || Object.entries(d.common).some(([k, v]) => k !== "mode" && !SHA.test(v))
43
+ || !text(d.proposer) || !text(d.judge) || !Array.isArray(d.arms) || d.arms.length < 2 || d.arms.length > 8
44
+ || !Array.isArray(d.cases) || !d.cases.length || d.cases.length > 128)
45
+ throw new Error("invalid intervention manifest");
46
+ for (const arm of d.arms)
47
+ if (!closed(arm, ["id", "configuration"]) || !text(arm.id, 128) || !closed(arm.configuration, axes)
48
+ || !text(arm.configuration.model) || !text(arm.configuration.effort) || ["skill", "prompt", "configuration"].some(k => !SHA.test(arm.configuration[k])))
49
+ throw new Error("invalid intervention arm");
50
+ if (new Set(d.arms.map(a => a.id)).size !== d.arms.length || new Set(d.cases.map(c => c.id)).size !== d.cases.length)
51
+ throw new Error("duplicate intervention identity");
52
+ for (const c of d.cases)
53
+ if (!closed(c, ["id", "criteria", "reps", "threshold", "critical"]) || !text(c.id, 128)
54
+ || !Number.isSafeInteger(c.criteria) || c.criteria < 1 || c.criteria > 128 || !Number.isSafeInteger(c.reps) || c.reps < 1 || c.reps > 20
55
+ || !Number.isFinite(c.threshold) || c.threshold <= 0 || c.threshold > 1 || typeof c.critical !== "boolean"
56
+ || ((c.critical || c.id.startsWith("B")) && c.threshold !== 1))
57
+ throw new Error("invalid frozen case policy");
58
+ const changedAxes = axes.filter(axis => d.arms.some(a => a.configuration[axis] !== d.arms[0].configuration[axis]));
59
+ if (changedAxes.some(axis => !d.axes.includes(axis)))
60
+ throw new Error("undeclared intervention axis");
61
+ const body = { ...d, version: "intervention-comparison-v1", inputDigest: digest({ common: d.common, cases: d.cases }), changedAxes, deterministicSampling: false };
62
+ return frozen({ ...body, id: digest(body) });
63
+ }
64
+ export const interventionEvidenceDigest = (value) => digest(value);
65
+ const assessments = new WeakSet();
66
+ const assessmentArtifacts = new WeakMap();
67
+ function manifestValid(m) {
68
+ const detached = clone(m);
69
+ const { version, id: _id, inputDigest: _inputs, changedAxes: _axes, deterministicSampling, ...draft } = detached;
70
+ if (version !== "intervention-comparison-v1" || deterministicSampling !== false || canonical(freezeIntervention(draft)) !== canonical(detached))
71
+ throw new Error("frozen intervention changed");
72
+ }
73
+ /** Pre-spend role gate: consumes independently resolved host facts, never guesses aliases or calls a provider. */
74
+ export function assertInterventionRoles(manifest, qualification) {
75
+ manifestValid(manifest);
76
+ if (!qualification || !qualification.proposer || !qualification.judge || !qualification.subjects || qualification.manifestId !== manifest.id || qualification.proposer.requested !== manifest.proposer || qualification.judge.requested !== manifest.judge
77
+ || !text(qualification.proposer.canonical) || !text(qualification.judge.canonical))
78
+ throw new Error("independent role qualification required");
79
+ const reserved = new Set([qualification.proposer.canonical, qualification.judge.canonical]);
80
+ if (reserved.size !== 2 || manifest.arms.some(a => !Object.hasOwn(qualification.subjects, a.id) || qualification.subjects[a.id].requested !== a.configuration.model
81
+ || !text(qualification.subjects[a.id].canonical) || reserved.has(qualification.subjects[a.id].canonical)))
82
+ throw new Error("role identity conflict or unresolved role");
83
+ }
84
+ /** Inspects separately host-qualified evidence; does not obtain qualification or execute a model. */
85
+ export function assessIntervention(manifest, evidence, qualification) {
86
+ assertInterventionRoles(manifest, qualification);
87
+ if (evidence.length > manifest.arms.length || new Set(evidence.map(e => e.armId)).size !== evidence.length || evidence.some(e => !manifest.arms.some(a => a.id === e.armId)))
88
+ throw new Error("unexpected intervention evidence identity");
89
+ const retained = new Map();
90
+ let retainedBytes = 0;
91
+ const arms = manifest.arms.map(arm => {
92
+ const e = evidence.find(e => e.armId === arm.id);
93
+ let admitted = false;
94
+ const result = (state) => ({ armId: arm.id, state, eligible: state === "MEASURED", artifactDigests: admitted ? [...e.artifactDigests] : [], cost: admitted ? e.cost : null, costUnit: admitted ? e.costUnit : "unknown" });
95
+ if (!e)
96
+ return result("MISSING");
97
+ if (qualification.evidenceDigests[arm.id] !== interventionEvidenceDigest(e) || !closed(e, ["armId", "inputDigest", "artifactDigests", "cells", "cost", "costUnit"])
98
+ || e.inputDigest !== manifest.inputDigest || !Array.isArray(e.artifactDigests) || e.artifactDigests.length > 4096 || e.artifactDigests.some(h => !SHA.test(h))
99
+ || !(e.cost === null || (Number.isFinite(e.cost) && e.cost >= 0)) || e.costUnit !== manifest.resourceMetric || !Array.isArray(e.cells)
100
+ || new Set(e.artifactDigests).size !== e.artifactDigests.length || e.cells.length !== manifest.cases.reduce((n, c) => n + c.reps, 0))
101
+ return result("ERROR");
102
+ admitted = true;
103
+ const cells = new Map();
104
+ let error = false, unmeasured = false, unresolved = false, missingOutput = false;
105
+ for (const c of e.cells) {
106
+ const spec = manifest.cases.find(s => s.id === c.caseId), id = `${c.caseId}:${c.repetition}`;
107
+ if (!closed(c, ["caseId", "repetition", "delivery", "objective", "criteria", "suspect", "artifactSha256"]) || !spec || !Number.isSafeInteger(c.repetition) || c.repetition < 0 || c.repetition >= spec.reps || cells.has(id)
108
+ || !["PASS", "NOT-MEASURED", "ERROR"].includes(c.delivery) || !["PASS", "FAIL", "ERROR", "NOT-MEASURED"].includes(c.objective) || !Array.isArray(c.criteria) || typeof c.suspect !== "boolean") {
109
+ error = true;
110
+ continue;
111
+ }
112
+ cells.set(id, c);
113
+ if (c.artifactSha256 !== null && (typeof c.artifactSha256 !== "string" || !SHA.test(c.artifactSha256) || !e.artifactDigests.includes(c.artifactSha256)))
114
+ error = true;
115
+ if (c.delivery === "PASS" && c.objective === "PASS" && c.artifactSha256 === null)
116
+ missingOutput = true;
117
+ if (c.delivery !== "PASS" || c.objective !== "PASS") {
118
+ if (c.criteria.length)
119
+ error = true;
120
+ if (c.delivery === "NOT-MEASURED")
121
+ unmeasured = true;
122
+ if (c.delivery === "ERROR" || c.objective === "ERROR" || (c.objective === "NOT-MEASURED" && c.delivery !== "NOT-MEASURED"))
123
+ error = true;
124
+ }
125
+ else if (c.criteria.length !== spec.criteria || c.criteria.some(v => !["PASS", "FAIL", "UNKNOWN"].includes(v)))
126
+ error = true;
127
+ else if (c.suspect || c.criteria.includes("UNKNOWN"))
128
+ unresolved = true;
129
+ }
130
+ if (error)
131
+ return result("ERROR");
132
+ if (unmeasured)
133
+ return result("NOT-MEASURED");
134
+ if (unresolved)
135
+ return result("UNRESOLVED");
136
+ if (missingOutput)
137
+ return result("MISSING");
138
+ const passed = manifest.cases.every(spec => Array.from(cells.values()).filter(c => c.caseId === spec.id && c.objective === "PASS" && c.criteria.every(v => v === "PASS")).length / spec.reps >= spec.threshold);
139
+ if (!passed)
140
+ return result("FAILED");
141
+ if (!e.artifactDigests.length)
142
+ return result("MISSING");
143
+ for (const hash of e.artifactDigests) {
144
+ const bytes = qualification.artifacts?.get(hash);
145
+ if (bytes === undefined)
146
+ return result("MISSING");
147
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength > 8 * 1024 * 1024)
148
+ return result("ERROR");
149
+ const stable = Buffer.from(bytes);
150
+ if (createHash("sha256").update(stable).digest("hex") !== hash)
151
+ return result("ERROR");
152
+ if (!retained.has(hash)) {
153
+ if (retainedBytes + bytes.byteLength > 64 * 1024 * 1024)
154
+ return result("ERROR");
155
+ retained.set(hash, stable);
156
+ retainedBytes += stable.byteLength;
157
+ }
158
+ }
159
+ return result("MEASURED");
160
+ });
161
+ const eligible = arms.filter(a => a.eligible), knownCosts = eligible.length && eligible.every(a => a.cost !== null && a.costUnit === eligible[0].costUnit);
162
+ const cheapestEligible = knownCosts ? [...eligible].sort((a, b) => a.cost - b.cost || (a.armId < b.armId ? -1 : 1))[0].armId : null;
163
+ const result = frozen({ manifestId: manifest.id, complete: arms.every(a => a.state === "MEASURED" || a.state === "FAILED"), arms, cheapestEligible, routingDefault: null, adoptionAuthorized: false });
164
+ assessments.add(result);
165
+ assessmentArtifacts.set(result, retained);
166
+ return result;
167
+ }
168
+ /** Only this view omits identity/cost; arbitrary artifact content can still leak clues and must be reviewed. */
169
+ export function createBlindComparison(manifest, assessment, fixtureOrPersistedSeed) {
170
+ manifestValid(manifest);
171
+ if (!assessments.has(assessment) || assessment.manifestId !== manifest.id || !assessment.complete)
172
+ throw new Error("complete recomputed intervention assessment required");
173
+ const seed = fixtureOrPersistedSeed ?? randomBytes(32).toString("hex");
174
+ if (!SHA.test(seed))
175
+ throw new Error("invalid private blinding seed");
176
+ const assessmentId = digest(assessment);
177
+ const eligible = assessment.arms.filter(a => a.eligible).sort((a, b) => {
178
+ const left = digest([seed, manifest.id, assessmentId, a.armId]), right = digest([seed, manifest.id, assessmentId, b.armId]);
179
+ return left < right ? -1 : left > right ? 1 : 0;
180
+ });
181
+ const mapping = new Map(eligible.map((a, i) => [`variant-${digest([seed, manifest.id, assessmentId, i]).slice(0, 16)}`, a.armId]));
182
+ if (mapping.size !== eligible.length)
183
+ throw new Error("opaque label collision");
184
+ const cards = [...mapping].map(([label, armId]) => ({ label, artifactDigests: assessment.arms.find(a => a.armId === armId).artifactDigests }));
185
+ let choice = null, revealed = false;
186
+ return Object.freeze({
187
+ view: () => frozen({ version: "blind-intervention-view-v1", cards, limitations: ["artifact-content-may-disclose-identity"] }),
188
+ readArtifact(label, hash) {
189
+ const armId = mapping.get(label), arm = assessment.arms.find(a => a.armId === armId);
190
+ if (!arm || !arm.artifactDigests.includes(hash))
191
+ throw new Error("artifact outside blind view");
192
+ const bytes = assessmentArtifacts.get(assessment)?.get(hash);
193
+ if (!bytes)
194
+ throw new Error("retained blind artifact missing");
195
+ return Buffer.from(bytes);
196
+ },
197
+ choose(input) {
198
+ const value = clone(input);
199
+ if (!closed(value, ["kind", "labels"]) || !["one", "tie", "none", "insufficient"].includes(value.kind) || !Array.isArray(value.labels)
200
+ || new Set(value.labels).size !== value.labels.length || value.labels.some(l => !mapping.has(l))
201
+ || (value.kind === "one" ? value.labels.length !== 1 : value.kind === "tie" ? value.labels.length < 2 : value.labels.length !== 0))
202
+ throw new Error("invalid blind quality choice");
203
+ value.labels.sort();
204
+ if (revealed && canonical(choice) !== canonical(value))
205
+ throw new Error("quality choice locked after reveal");
206
+ choice = frozen(value);
207
+ return choice;
208
+ },
209
+ reveal() {
210
+ if (!choice)
211
+ throw new Error("quality choice required before reveal");
212
+ revealed = true;
213
+ return frozen({ manifestId: manifest.id, choice, arms: [...mapping].map(([label, armId]) => ({ label, armId, configuration: manifest.arms.find(a => a.id === armId).configuration,
214
+ cost: assessment.arms.find(a => a.armId === armId).cost, costUnit: assessment.arms.find(a => a.armId === armId).costUnit })), routingDefault: null });
215
+ },
216
+ });
217
+ }
218
+ //# sourceMappingURL=intervention.js.map
@@ -0,0 +1,94 @@
1
+ /** Bounded operator envelope; duplicate keys and over-expanded/cyclic objects are refused before dispatch. */
2
+ export declare function parseLearningRequest(text: string): unknown;
3
+ export interface HypothesisProposal {
4
+ archiveSnapshot: string;
5
+ caseIds: string[];
6
+ population: string;
7
+ intervention: string;
8
+ alternatives: string[];
9
+ prediction: string;
10
+ downside: string;
11
+ disproof: string;
12
+ rollback: string;
13
+ limits: {
14
+ subjectCalls: number;
15
+ judgeCalls: number;
16
+ wallMs: number;
17
+ };
18
+ effectProfile: string | null;
19
+ }
20
+ export interface Hypothesis {
21
+ version: "factory-hypothesis-v1";
22
+ id: string;
23
+ proposal: HypothesisProposal;
24
+ status: "proposed";
25
+ }
26
+ export declare function buildHypothesis(input: HypothesisProposal): Hypothesis;
27
+ export interface InvestigationAuthority {
28
+ id: string;
29
+ investigations: readonly string[];
30
+ promotions: readonly string[];
31
+ }
32
+ /** Independent host declarations, not approval fields on a proposal and never adoption/activation authority. */
33
+ export declare function authorizeInvestigation(h: Hypothesis, authority: InvestigationAuthority | null): {
34
+ version: string;
35
+ hypothesisId: string;
36
+ authorityId: string;
37
+ scope: "investigation-only";
38
+ };
39
+ export interface InvestigationScenarioPreview {
40
+ version: "investigation-scenario-preview-v1";
41
+ hypothesisId: string;
42
+ specPath: string;
43
+ baseSha256: string;
44
+ afterSha256: string;
45
+ scenario: Record<string, unknown>;
46
+ block: string;
47
+ digest: string;
48
+ }
49
+ export declare function previewInvestigationScenario(h: Hypothesis, specPath: string, scenario: Record<string, unknown>): InvestigationScenarioPreview;
50
+ /** Exact preview consent includes destination, existing spec and complete scenario bytes. No automatic sanitization claim. */
51
+ export declare function applyInvestigationScenario(h: Hypothesis, preview: InvestigationScenarioPreview, authority: InvestigationAuthority | null): {
52
+ scenarioId: string;
53
+ replayed: boolean;
54
+ sha256: string;
55
+ scope: "scenario-only";
56
+ };
57
+ export interface FrozenInvestigationInputs {
58
+ specSha256: string;
59
+ rubricSha256: string;
60
+ judgePolicySha256: string;
61
+ heldoutSha256: string;
62
+ configurationSha256: string;
63
+ }
64
+ export declare function freezeInvestigation(h: Hypothesis, inputs: FrozenInvestigationInputs, authority: InvestigationAuthority | null): {
65
+ digest: string;
66
+ version: string;
67
+ hypothesisId: string;
68
+ authorityId: string;
69
+ inputs: FrozenInvestigationInputs;
70
+ executionReady: false;
71
+ };
72
+ export declare function assertFrozenInvestigation(frozen: ReturnType<typeof freezeInvestigation>, current: FrozenInvestigationInputs): void;
73
+ export interface WeeklyInvestigationInput {
74
+ week: string;
75
+ population: string;
76
+ policyDigest: string;
77
+ archiveSnapshot: string;
78
+ eligibleCaseIds: string[];
79
+ maxCases: number;
80
+ }
81
+ export interface WeeklyInvestigationSelection {
82
+ version: "weekly-investigation-selection-v1";
83
+ id: string;
84
+ key: string;
85
+ inputDigest: string;
86
+ week: string;
87
+ population: string;
88
+ policyDigest: string;
89
+ archiveSnapshot: string;
90
+ selectedCaseIds: string[];
91
+ sampling: "exploratory-sha256-order-v1";
92
+ }
93
+ /** The host persists these immutable selections; this function neither schedules a process nor spends tokens. */
94
+ export declare function selectWeeklyInvestigation(history: readonly WeeklyInvestigationSelection[], input: WeeklyInvestigationInput): WeeklyInvestigationSelection;
@@ -0,0 +1,148 @@
1
+ import { createHash } from "node:crypto";
2
+ import yaml from "js-yaml";
3
+ import { readFileSync, realpathSync } from "node:fs";
4
+ import { appendScenario, renderScenarioBlock, specSha256 } from "./spec-write.js";
5
+ import { parseSpec } from "./spec.js";
6
+ const SHA = /^[a-f0-9]{64}$/;
7
+ const keys = (x, expected) => Object.keys(x).sort().join() === [...expected].sort().join();
8
+ const text = (x, max = 4000) => typeof x === "string" && x.length > 0 && x.length <= max;
9
+ function json(input) {
10
+ let nodes = 0;
11
+ const visit = (x, depth) => {
12
+ if (++nodes > 4096 || depth > 16)
13
+ throw new Error("investigation JSON exceeds bounds");
14
+ if (x === null || typeof x === "boolean" || typeof x === "string" || (typeof x === "number" && Number.isFinite(x)))
15
+ return x;
16
+ if (!x || typeof x !== "object" || (!Array.isArray(x) && ![Object.prototype, null].includes(Object.getPrototypeOf(x))))
17
+ throw new Error("plain investigation JSON required");
18
+ const descriptors = Object.getOwnPropertyDescriptors(x);
19
+ if (Array.isArray(x)) {
20
+ if (Object.getPrototypeOf(x) !== Array.prototype || x.length > 256 || Reflect.ownKeys(x).length !== x.length + 1)
21
+ throw new Error("bounded dense JSON array required");
22
+ return Array.from({ length: x.length }, (_, i) => { const d = descriptors[String(i)]; if (!d || !('value' in d))
23
+ throw new Error("plain JSON array required"); return visit(d.value, depth + 1); });
24
+ }
25
+ const out = {};
26
+ for (const key of Reflect.ownKeys(descriptors).sort((a, b) => String(a) < String(b) ? -1 : 1)) {
27
+ const d = descriptors[key];
28
+ if (typeof key !== "string" || !d.enumerable || !('value' in d))
29
+ throw new Error("plain JSON properties required");
30
+ Object.defineProperty(out, key, { value: visit(d.value, depth + 1), enumerable: true });
31
+ }
32
+ return out;
33
+ };
34
+ const result = visit(input, 0);
35
+ if (Buffer.byteLength(JSON.stringify(result)) > 65536)
36
+ throw new Error("investigation JSON exceeds byte bound");
37
+ return result;
38
+ }
39
+ /** Bounded operator envelope; duplicate keys and over-expanded/cyclic objects are refused before dispatch. */
40
+ export function parseLearningRequest(text) {
41
+ if (typeof text !== "string" || Buffer.byteLength(text) > 65536)
42
+ throw new Error("learning request exceeds bound");
43
+ let value;
44
+ try {
45
+ value = yaml.load(text, { schema: yaml.JSON_SCHEMA, json: false });
46
+ }
47
+ catch {
48
+ throw new Error("invalid learning request");
49
+ }
50
+ return json(value);
51
+ }
52
+ const digest = (x) => createHash("sha256").update(JSON.stringify(json(x))).digest("hex");
53
+ function freeze(value) { if (value && typeof value === "object") {
54
+ Object.values(value).forEach(freeze);
55
+ Object.freeze(value);
56
+ } return value; }
57
+ export function buildHypothesis(input) {
58
+ const p = json(input);
59
+ if (!keys(p, ["archiveSnapshot", "caseIds", "population", "intervention", "alternatives", "prediction", "downside", "disproof", "rollback", "limits", "effectProfile"])
60
+ || !SHA.test(p.archiveSnapshot) || !Array.isArray(p.caseIds) || !p.caseIds.length || p.caseIds.some(id => typeof id !== "string" || !SHA.test(id))
61
+ || ![p.population, p.intervention, p.prediction, p.downside, p.disproof, p.rollback].every(s => text(s))
62
+ || !Array.isArray(p.alternatives) || !p.alternatives.length || p.alternatives.some(a => !text(a))
63
+ || !p.limits || !keys(p.limits, ["subjectCalls", "judgeCalls", "wallMs"])
64
+ || !Object.values(p.limits).every(n => Number.isSafeInteger(n) && n >= 0 && n <= 1000000000)
65
+ || !(p.effectProfile === null || text(p.effectProfile, 512)))
66
+ throw new Error("invalid bounded hypothesis");
67
+ return freeze({ version: "factory-hypothesis-v1", id: digest({ version: "factory-hypothesis-v1", proposal: p }), proposal: p, status: "proposed" });
68
+ }
69
+ function authorize(h, authority) {
70
+ const value = json(h);
71
+ if (!keys(value, ["version", "id", "proposal", "status"]) || value.version !== "factory-hypothesis-v1" || value.status !== "proposed" || buildHypothesis(value.proposal).id !== value.id)
72
+ throw new Error("hypothesis identity changed");
73
+ if (!authority || !text(authority.id, 512) || !Array.isArray(authority.investigations) || !Array.isArray(authority.promotions)
74
+ || authority.investigations.length > 256 || authority.promotions.length > 256
75
+ || [...authority.investigations, ...authority.promotions].some(id => typeof id !== "string" || !SHA.test(id))
76
+ || !authority.investigations.includes(value.id))
77
+ throw new Error("independent investigation authority required");
78
+ return authority;
79
+ }
80
+ /** Independent host declarations, not approval fields on a proposal and never adoption/activation authority. */
81
+ export function authorizeInvestigation(h, authority) {
82
+ const selected = authorize(h, authority);
83
+ return freeze({ version: "investigation-approval-v1", hypothesisId: h.id, authorityId: selected.id, scope: "investigation-only" });
84
+ }
85
+ export function previewInvestigationScenario(h, specPath, scenario) {
86
+ if (buildHypothesis(h.proposal).id !== h.id)
87
+ throw new Error("hypothesis identity changed");
88
+ const path = realpathSync(specPath), current = readFileSync(path, "utf8"), safe = json(scenario), block = renderScenarioBlock(safe);
89
+ parseSpec(current + block, path);
90
+ const value = { version: "investigation-scenario-preview-v1", hypothesisId: h.id, specPath: path, baseSha256: specSha256(current), afterSha256: specSha256(current + block), scenario: safe, block };
91
+ return freeze({ ...value, digest: digest(value) });
92
+ }
93
+ /** Exact preview consent includes destination, existing spec and complete scenario bytes. No automatic sanitization claim. */
94
+ export function applyInvestigationScenario(h, preview, authority) {
95
+ const selected = authorize(h, authority), copy = json(preview), { digest: claimed, ...body } = copy;
96
+ if (!keys(copy, ["version", "hypothesisId", "specPath", "baseSha256", "afterSha256", "scenario", "block", "digest"])
97
+ || body.version !== "investigation-scenario-preview-v1" || body.hypothesisId !== h.id || digest(body) !== claimed
98
+ || renderScenarioBlock(body.scenario) !== body.block || !selected.promotions.includes(claimed))
99
+ throw new Error("exact privacy/promotion preview authority required");
100
+ if (realpathSync(body.specPath) !== body.specPath)
101
+ throw new Error("promotion destination changed");
102
+ const current = readFileSync(body.specPath, "utf8");
103
+ if (specSha256(current) === body.afterSha256)
104
+ return { scenarioId: String(body.scenario.id), replayed: true, sha256: body.afterSha256, scope: "scenario-only" };
105
+ const result = appendScenario({ specPath: body.specPath, scenario: body.scenario, baseSha256: body.baseSha256 });
106
+ return { scenarioId: result.id, replayed: false, sha256: result.sha256, scope: "scenario-only" };
107
+ }
108
+ function frozenInputs(input) {
109
+ const value = json(input);
110
+ if (!keys(value, ["specSha256", "rubricSha256", "judgePolicySha256", "heldoutSha256", "configurationSha256"]) || Object.values(value).some(v => typeof v !== "string" || !SHA.test(v)))
111
+ throw new Error("invalid frozen evaluation inputs");
112
+ return value;
113
+ }
114
+ export function freezeInvestigation(h, inputs, authority) {
115
+ const approved = authorizeInvestigation(h, authority), value = { version: "frozen-investigation-v1", hypothesisId: h.id, authorityId: approved.authorityId, inputs: frozenInputs(inputs), executionReady: false };
116
+ return freeze({ ...value, digest: digest(value) });
117
+ }
118
+ export function assertFrozenInvestigation(frozen, current) {
119
+ const { digest: recorded, ...value } = json(frozen);
120
+ if (!keys(value, ["version", "hypothesisId", "authorityId", "inputs", "executionReady"]) || !SHA.test(value.hypothesisId) || !text(value.authorityId, 512)
121
+ || value.version !== "frozen-investigation-v1" || value.executionReady !== false || digest(value) !== recorded || digest(value.inputs) !== digest(frozenInputs(current)))
122
+ throw new Error("frozen investigation changed or invalid");
123
+ }
124
+ /** The host persists these immutable selections; this function neither schedules a process nor spends tokens. */
125
+ export function selectWeeklyInvestigation(history, input) {
126
+ const value = json(input);
127
+ if (!keys(value, ["week", "population", "policyDigest", "archiveSnapshot", "eligibleCaseIds", "maxCases"])
128
+ || !/^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(value.week) || !text(value.population, 512)
129
+ || !SHA.test(value.policyDigest) || !SHA.test(value.archiveSnapshot) || !Array.isArray(value.eligibleCaseIds)
130
+ || !value.eligibleCaseIds.length || value.eligibleCaseIds.some(id => typeof id !== "string" || !SHA.test(id))
131
+ || !Number.isSafeInteger(value.maxCases) || value.maxCases < 1 || value.maxCases > 32 || history.length > 4096)
132
+ throw new Error("invalid weekly investigation selection");
133
+ const eligible = [...new Set(value.eligibleCaseIds)].sort();
134
+ const key = digest({ week: value.week, population: value.population, policyDigest: value.policyDigest });
135
+ const inputDigest = digest({ ...value, eligibleCaseIds: eligible });
136
+ const selectedCaseIds = eligible.sort((a, b) => { const ka = digest([key, value.archiveSnapshot, a]), kb = digest([key, value.archiveSnapshot, b]); return ka < kb ? -1 : ka > kb ? 1 : 0; }).slice(0, value.maxCases);
137
+ const body = { version: "weekly-investigation-selection-v1", key, inputDigest, week: value.week, population: value.population,
138
+ policyDigest: value.policyDigest, archiveSnapshot: value.archiveSnapshot, selectedCaseIds, sampling: "exploratory-sha256-order-v1" };
139
+ const result = freeze({ ...body, id: digest(body) });
140
+ const previous = history.filter(record => record.key === key);
141
+ for (const record of previous) {
142
+ const { id, ...old } = json(record);
143
+ if (digest(old) !== id || id !== result.id)
144
+ throw new Error("weekly investigation already frozen or history invalid");
145
+ }
146
+ return previous.length ? freeze(json(previous[0])) : result;
147
+ }
148
+ //# sourceMappingURL=investigation.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
+ }>;