@skill-harness/core 0.7.0 → 0.9.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.
- package/dist/adapters/types.d.ts +8 -0
- package/dist/adjudication.d.ts +1 -0
- package/dist/adjudication.js +26 -4
- package/dist/capture-trace-types.d.ts +19 -0
- package/dist/comparison.d.ts +96 -0
- package/dist/comparison.js +183 -0
- package/dist/execution-trace.js +93 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lint.d.ts +7 -0
- package/dist/lint.js +14 -2
- package/dist/regate.d.ts +1 -1
- package/dist/regate.js +94 -21
- package/dist/regrade.d.ts +4 -0
- package/dist/regrade.js +29 -5
- package/dist/report.d.ts +7 -0
- package/dist/report.js +6 -0
- package/dist/reps.d.ts +8 -1
- package/dist/reps.js +47 -13
- package/dist/rescore.js +2 -2
- package/dist/restamp.d.ts +68 -0
- package/dist/restamp.js +194 -0
- package/dist/results.d.ts +28 -0
- package/dist/results.js +56 -4
- package/dist/run.js +96 -35
- package/dist/sandbox.d.ts +32 -0
- package/dist/sandbox.js +12 -0
- package/dist/score.d.ts +3 -1
- package/dist/score.js +20 -5
- package/dist/seeded.d.ts +7 -1
- package/dist/seeded.js +29 -8
- package/dist/sources.d.ts +50 -0
- package/dist/sources.js +186 -7
- package/dist/spec.d.ts +15 -0
- package/dist/spec.js +53 -1
- package/dist/stability.js +26 -4
- package/dist/trace-gates.d.ts +2 -1
- package/dist/trace-gates.js +4 -1
- package/dist/trajectory-gates.d.ts +137 -0
- package/dist/trajectory-gates.js +592 -0
- package/package.json +5 -3
- package/schemas/results-v2.schema.json +133 -0
- package/schemas/specification-v1.schema.json +217 -0
- package/schemas/trajectory-event-v1.schema.json +61 -0
package/dist/spec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
3
|
import { parseTraceAssert } from "./trace-gates.js";
|
|
4
|
+
import { parseTrajectoryAssert } from "./trajectory-gates.js";
|
|
4
5
|
/** Thrown on any validation failure. Message always carries the spec file path. */
|
|
5
6
|
export class SpecError extends Error {
|
|
6
7
|
constructor(message, file) {
|
|
@@ -88,6 +89,46 @@ function resolveExtensions(env, hasSystemPrompt, id, file) {
|
|
|
88
89
|
* with empty-git or a fixture — asking for one on a bare cwd is an authoring mistake,
|
|
89
90
|
* not something to silently ignore.
|
|
90
91
|
*/
|
|
92
|
+
function resolveEventSources(env, id, file) {
|
|
93
|
+
const raw = env && typeof env === "object" ? env.event_sources : undefined;
|
|
94
|
+
if (raw === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
97
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources must be a non-empty list`, file);
|
|
98
|
+
}
|
|
99
|
+
const allowedAdapters = new Set([
|
|
100
|
+
"normalized-v1", "principal-assurance-v1", "pi-daddy-v1",
|
|
101
|
+
]);
|
|
102
|
+
return raw.map((entry, index) => {
|
|
103
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
104
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources[${index}] must be a mapping`, file);
|
|
105
|
+
}
|
|
106
|
+
const source = entry;
|
|
107
|
+
for (const key of Object.keys(source)) {
|
|
108
|
+
if (!["adapter", "path", "required"].includes(key)) {
|
|
109
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources[${index}] has unknown key \`${key}\``, file);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!allowedAdapters.has(source.adapter)) {
|
|
113
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources[${index}].adapter is unsupported`, file);
|
|
114
|
+
}
|
|
115
|
+
if (typeof source.path !== "string" || !source.path.trim()) {
|
|
116
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources[${index}].path must be non-empty`, file);
|
|
117
|
+
}
|
|
118
|
+
const path = source.path.trim();
|
|
119
|
+
if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.includes("\\") || path.split("/").some((part) => part === ".." || part === "." || part === "")) {
|
|
120
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources paths must be workspace-relative without traversal (got \`${path}\`)`, file);
|
|
121
|
+
}
|
|
122
|
+
if (source.required !== undefined && typeof source.required !== "boolean") {
|
|
123
|
+
throw new SpecError(`scenario \`${id}\` env.event_sources[${index}].required must be true or false`, file);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
adapter: source.adapter,
|
|
127
|
+
path,
|
|
128
|
+
required: source.required !== false,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
91
132
|
function resolveRemote(env, workspace, id, file) {
|
|
92
133
|
const raw = env && typeof env === "object" ? env.remote : undefined;
|
|
93
134
|
if (raw === undefined)
|
|
@@ -114,6 +155,9 @@ export function parseSpec(text, file) {
|
|
|
114
155
|
throw new SpecError("spec must be a YAML mapping", file);
|
|
115
156
|
}
|
|
116
157
|
const o = doc;
|
|
158
|
+
if (o.schema !== undefined && o.schema !== 1) {
|
|
159
|
+
throw new SpecError(`unsupported \`schema\` ${JSON.stringify(o.schema)} (expected 1)`, file);
|
|
160
|
+
}
|
|
117
161
|
if (typeof o.skill !== "string" || o.skill.length === 0) {
|
|
118
162
|
throw new SpecError("missing or invalid `skill` (string)", file);
|
|
119
163
|
}
|
|
@@ -179,6 +223,9 @@ export function parseSpec(text, file) {
|
|
|
179
223
|
if (rawAssert?.trace !== undefined) {
|
|
180
224
|
scenario.traceAssert = parseTraceAssert(rawAssert.trace, `${file}: scenario \`${id}\``);
|
|
181
225
|
}
|
|
226
|
+
if (rawAssert?.trajectory !== undefined) {
|
|
227
|
+
scenario.trajectoryAssert = parseTrajectoryAssert(rawAssert.trajectory, `${file}: scenario \`${id}\``);
|
|
228
|
+
}
|
|
182
229
|
if (mode === "seeded") {
|
|
183
230
|
if (typeof s.fixture !== "string" || s.fixture.length === 0) {
|
|
184
231
|
throw new SpecError(`seeded scenario \`${id}\` requires a \`fixture\` path`, file);
|
|
@@ -231,6 +278,10 @@ export function parseSpec(text, file) {
|
|
|
231
278
|
}
|
|
232
279
|
scenario.workspace = resolveWorkspace(s.env, mode, scenario.fixture, id, file);
|
|
233
280
|
scenario.remote = resolveRemote(s.env, scenario.workspace, id, file);
|
|
281
|
+
scenario.eventSources = resolveEventSources(s.env, id, file);
|
|
282
|
+
if (scenario.eventSources && !scenario.trajectoryAssert) {
|
|
283
|
+
throw new SpecError(`scenario \`${id}\` declares env.event_sources without assert.trajectory`, file);
|
|
284
|
+
}
|
|
234
285
|
if (s.system_prompt_file !== undefined) {
|
|
235
286
|
if (typeof s.system_prompt_file !== "string" || !s.system_prompt_file.trim()) {
|
|
236
287
|
throw new SpecError(`scenario \`${id}\` \`system_prompt_file\` must be a non-empty string`, file);
|
|
@@ -277,7 +328,8 @@ export function parseSpec(text, file) {
|
|
|
277
328
|
}
|
|
278
329
|
return scenario;
|
|
279
330
|
});
|
|
280
|
-
|
|
331
|
+
const effectiveCritical = [...new Set([...critical, ...scenarios.filter((scenario) => scenario.critical).map((scenario) => scenario.id)])];
|
|
332
|
+
return { schema: 1, skill: o.skill, judge_persona: o.judge_persona, ship_bar, critical: effectiveCritical, scenarios };
|
|
281
333
|
}
|
|
282
334
|
/** Load + validate a specification.yaml from disk. */
|
|
283
335
|
export function loadSpec(file) {
|
package/dist/stability.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { loadSpec } from "./spec.js";
|
|
3
3
|
import { effectiveVerdicts } from "./results.js";
|
|
4
4
|
import { collectScoredRuns } from "./trends.js";
|
|
5
|
-
import { describeSourceKey, scenarioSourceKeys, PERSONA_KEY, SKILL_KEY } from "./sources.js";
|
|
5
|
+
import { describeSourceKey, isSupersededKey, scenarioSourceKeys, PERSONA_KEY, SKILL_KEY, SKILL_PROMPT_KEY } from "./sources.js";
|
|
6
6
|
const DEFAULT_WINDOW = 5;
|
|
7
7
|
/** A verdict that carries evidence about the task rather than about the harness or judge. */
|
|
8
8
|
function conclusive(v) {
|
|
@@ -23,6 +23,25 @@ function pointFor(r, s, verdict) {
|
|
|
23
23
|
unanimous: reps > 1 && s.flakiness === 0,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Did the skill text the MODEL RECEIVES differ between two runs?
|
|
28
|
+
*
|
|
29
|
+
* Prefers `skill:prompt` when both runs recorded it, and falls back to the raw-bytes
|
|
30
|
+
* `SKILL.md` key for runs that predate it. A frontmatter-only edit moves the raw bytes and
|
|
31
|
+
* not the prompt, and labelling a flip "SKILL.md changed across that step" on that basis
|
|
32
|
+
* would point a reader at an edit no model could have seen — the note's whole job is to
|
|
33
|
+
* say which of two explanations the record supports, so it must not invent a third.
|
|
34
|
+
*/
|
|
35
|
+
function skillTextChanged(prev, cur) {
|
|
36
|
+
for (const key of [SKILL_PROMPT_KEY, SKILL_KEY]) {
|
|
37
|
+
const a = prev?.[key];
|
|
38
|
+
const b = cur?.[key];
|
|
39
|
+
if (a === undefined || b === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
return a !== b;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
26
45
|
/** reps + threshold, normalised the way lift.ts normalises it (a lone rep has no threshold). */
|
|
27
46
|
function shapeOf(s) {
|
|
28
47
|
const reps = s.reps ?? 1;
|
|
@@ -43,6 +62,11 @@ function compareSources(a, b, keys) {
|
|
|
43
62
|
let shared = 0;
|
|
44
63
|
const changed = [];
|
|
45
64
|
for (const key of keys) {
|
|
65
|
+
// Same rule as lint's: when both runs recorded the model-visible digest of a prompt
|
|
66
|
+
// document, THAT is the comparison. Leaving the raw-bytes key in would call two runs
|
|
67
|
+
// "different questions" over an `allowed-tools:` line neither model could read.
|
|
68
|
+
if (isSupersededKey(key, a) && isSupersededKey(key, b))
|
|
69
|
+
continue;
|
|
46
70
|
const va = a[key];
|
|
47
71
|
const vb = b[key];
|
|
48
72
|
if (va === undefined || vb === undefined)
|
|
@@ -77,9 +101,7 @@ function stabilityForScenario(group, scenario, window) {
|
|
|
77
101
|
const cur = raw[i];
|
|
78
102
|
const from = points[i - 1];
|
|
79
103
|
const to = points[i];
|
|
80
|
-
const skillChanged = prev.r.source_hashes
|
|
81
|
-
cur.r.source_hashes?.[SKILL_KEY] !== undefined &&
|
|
82
|
-
prev.r.source_hashes[SKILL_KEY] !== cur.r.source_hashes[SKILL_KEY];
|
|
104
|
+
const skillChanged = skillTextChanged(prev.r.source_hashes, cur.r.source_hashes);
|
|
83
105
|
const base = { from, to, flipped: false, skillChanged, changedSources: [], unanimousFlip: false };
|
|
84
106
|
if (!prev.ok || !cur.ok) {
|
|
85
107
|
pairs.push({ ...base, status: "inconclusive" });
|
package/dist/trace-gates.d.ts
CHANGED
|
@@ -95,7 +95,7 @@ export declare function normalizeSubagentCall(args: Record<string, unknown>): Su
|
|
|
95
95
|
*/
|
|
96
96
|
export type AssertionStatus = "PASS" | "FAIL" | "ERROR";
|
|
97
97
|
export interface AssertionResult {
|
|
98
|
-
kind: "require_call" | "require_subagent" | "forbid_call" | "unchanged_path";
|
|
98
|
+
kind: "require_call" | "require_subagent" | "forbid_call" | "unchanged_path" | "trace_evidence";
|
|
99
99
|
status: AssertionStatus;
|
|
100
100
|
detail: string;
|
|
101
101
|
}
|
|
@@ -131,3 +131,4 @@ export declare function matchesGlob(pattern: string, path: string): boolean;
|
|
|
131
131
|
* possible failure for a safety check.
|
|
132
132
|
*/
|
|
133
133
|
export declare function parseTraceAssert(raw: unknown, ctx: string): TraceAssert;
|
|
134
|
+
export declare function parsePredicate(raw: unknown, ctx: string): ArgPredicate;
|
package/dist/trace-gates.js
CHANGED
|
@@ -39,6 +39,9 @@ export function normalizeSubagentCall(args) {
|
|
|
39
39
|
*/
|
|
40
40
|
export function evaluateTraceGates(assert, trace) {
|
|
41
41
|
const assertions = [];
|
|
42
|
+
for (const error of trace.capture_errors ?? []) {
|
|
43
|
+
assertions.push({ kind: "trace_evidence", status: "ERROR", detail: error });
|
|
44
|
+
}
|
|
42
45
|
for (const req of assert.require_calls ?? []) {
|
|
43
46
|
const matched = trace.tool_calls.filter((c) => c.name === req.tool && argsMatch(c, req.args));
|
|
44
47
|
const min = req.count?.min ?? 1;
|
|
@@ -469,7 +472,7 @@ function parseArgs(raw, ctx) {
|
|
|
469
472
|
}
|
|
470
473
|
return out;
|
|
471
474
|
}
|
|
472
|
-
function parsePredicate(raw, ctx) {
|
|
475
|
+
export function parsePredicate(raw, ctx) {
|
|
473
476
|
// `agent: plan` is shorthand for `agent: { equals: plan }` — the common case
|
|
474
477
|
// should not require the author to know the operator vocabulary.
|
|
475
478
|
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type ArgPredicate, type CountConstraint } from "./trace-gates.js";
|
|
2
|
+
/** Adapter-neutral event contract used by objective trajectory assertions. */
|
|
3
|
+
export declare const TRAJECTORY_EVENT_VERSION: "1.0";
|
|
4
|
+
export declare const TRAJECTORY_ASSERT_VERSION: "1.0";
|
|
5
|
+
export interface TrajectoryApproval {
|
|
6
|
+
id?: string;
|
|
7
|
+
capability?: string;
|
|
8
|
+
subject?: string;
|
|
9
|
+
source?: string;
|
|
10
|
+
scope?: string;
|
|
11
|
+
approved_at?: string;
|
|
12
|
+
expires_at?: string;
|
|
13
|
+
used_at?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface TrajectoryDigests {
|
|
16
|
+
plan?: string;
|
|
17
|
+
task?: string;
|
|
18
|
+
definition?: string;
|
|
19
|
+
head?: string;
|
|
20
|
+
tree?: string;
|
|
21
|
+
[key: string]: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One normalized workflow event. Native field names remain available in
|
|
25
|
+
* `attributes`; common correlation fields are promoted so the evaluator never
|
|
26
|
+
* needs to know whether the source was pi, pi-daddy, or principal assurance.
|
|
27
|
+
*/
|
|
28
|
+
export interface TrajectoryEventV1 {
|
|
29
|
+
event_version: typeof TRAJECTORY_EVENT_VERSION;
|
|
30
|
+
seq: number;
|
|
31
|
+
type: string;
|
|
32
|
+
source: string;
|
|
33
|
+
at?: string;
|
|
34
|
+
run_id?: string;
|
|
35
|
+
task_id?: string;
|
|
36
|
+
workspace_id?: string;
|
|
37
|
+
context_id?: string;
|
|
38
|
+
finding_id?: string;
|
|
39
|
+
parent_id?: string;
|
|
40
|
+
child_id?: string;
|
|
41
|
+
phase?: string;
|
|
42
|
+
tool?: string;
|
|
43
|
+
capability?: string;
|
|
44
|
+
requested_capabilities?: string[];
|
|
45
|
+
effective_capabilities?: string[];
|
|
46
|
+
refusal_code?: string;
|
|
47
|
+
exit_code?: number;
|
|
48
|
+
digests?: TrajectoryDigests;
|
|
49
|
+
approval?: TrajectoryApproval;
|
|
50
|
+
requirements?: string[];
|
|
51
|
+
attributes?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
export interface EventSelector {
|
|
54
|
+
event: string;
|
|
55
|
+
where?: Record<string, ArgPredicate>;
|
|
56
|
+
select?: "first" | "last" | "all";
|
|
57
|
+
}
|
|
58
|
+
export interface RequiredEvent extends EventSelector {
|
|
59
|
+
count?: CountConstraint;
|
|
60
|
+
}
|
|
61
|
+
export interface CorrelationAssert {
|
|
62
|
+
left: EventSelector;
|
|
63
|
+
right: EventSelector;
|
|
64
|
+
same?: string[];
|
|
65
|
+
different?: string[];
|
|
66
|
+
order?: "before" | "after";
|
|
67
|
+
}
|
|
68
|
+
export interface FreshnessAssert {
|
|
69
|
+
subject: EventSelector;
|
|
70
|
+
after: EventSelector[];
|
|
71
|
+
same?: string[];
|
|
72
|
+
}
|
|
73
|
+
export interface UniqueAssert {
|
|
74
|
+
events: EventSelector;
|
|
75
|
+
fields: string[];
|
|
76
|
+
}
|
|
77
|
+
export interface ForbidAfterAssert {
|
|
78
|
+
anchor: EventSelector;
|
|
79
|
+
forbidden: EventSelector[];
|
|
80
|
+
same?: string[];
|
|
81
|
+
anchor_optional?: boolean;
|
|
82
|
+
}
|
|
83
|
+
export interface ApprovalAssert {
|
|
84
|
+
grant: EventSelector;
|
|
85
|
+
use: EventSelector;
|
|
86
|
+
same?: string[];
|
|
87
|
+
scopes?: string[];
|
|
88
|
+
sources?: string[];
|
|
89
|
+
unexpired?: boolean;
|
|
90
|
+
max_uses?: number;
|
|
91
|
+
}
|
|
92
|
+
export interface CoverageAssert {
|
|
93
|
+
requirements: string[];
|
|
94
|
+
events?: EventSelector;
|
|
95
|
+
}
|
|
96
|
+
export interface TrajectoryAssert {
|
|
97
|
+
version: typeof TRAJECTORY_ASSERT_VERSION;
|
|
98
|
+
require?: RequiredEvent[];
|
|
99
|
+
forbid?: EventSelector[];
|
|
100
|
+
ordered?: EventSelector[][];
|
|
101
|
+
correlate?: CorrelationAssert[];
|
|
102
|
+
freshness?: FreshnessAssert[];
|
|
103
|
+
unique?: UniqueAssert[];
|
|
104
|
+
forbid_after?: ForbidAfterAssert[];
|
|
105
|
+
approvals?: ApprovalAssert[];
|
|
106
|
+
coverage?: CoverageAssert[];
|
|
107
|
+
}
|
|
108
|
+
export type TrajectoryAssertionStatus = "PASS" | "FAIL" | "ERROR";
|
|
109
|
+
export interface TrajectoryAssertionResult {
|
|
110
|
+
kind: "require_event" | "forbid_event" | "ordered_events" | "correlation" | "freshness" | "unique" | "forbid_after" | "approval" | "coverage" | "evidence";
|
|
111
|
+
status: TrajectoryAssertionStatus;
|
|
112
|
+
detail: string;
|
|
113
|
+
}
|
|
114
|
+
export interface TrajectoryGateResult {
|
|
115
|
+
status: TrajectoryAssertionStatus;
|
|
116
|
+
event_version: typeof TRAJECTORY_EVENT_VERSION;
|
|
117
|
+
events_sha256: string;
|
|
118
|
+
assertions: TrajectoryAssertionResult[];
|
|
119
|
+
}
|
|
120
|
+
export declare function trajectoryEventsSha256(events: TrajectoryEventV1[]): string;
|
|
121
|
+
export declare function serializeTrajectoryEvents(events: TrajectoryEventV1[]): string;
|
|
122
|
+
export declare function deserializeTrajectoryEvents(text: string): TrajectoryEventV1[] | null;
|
|
123
|
+
/** Evaluate a closed, declarative assertion set over saved normalized events. */
|
|
124
|
+
export declare function evaluateTrajectoryGates(assert: TrajectoryAssert, input: TrajectoryEventV1[]): TrajectoryGateResult;
|
|
125
|
+
export declare function parseTrajectoryAssert(raw: unknown, ctx: string): TrajectoryAssert;
|
|
126
|
+
export interface MutationSelfTestCase {
|
|
127
|
+
id: string;
|
|
128
|
+
detected: boolean;
|
|
129
|
+
status: TrajectoryAssertionStatus;
|
|
130
|
+
detail: string;
|
|
131
|
+
}
|
|
132
|
+
export interface MutationSelfTestReport {
|
|
133
|
+
baseline: "PASS";
|
|
134
|
+
cases: MutationSelfTestCase[];
|
|
135
|
+
}
|
|
136
|
+
/** Fast, deterministic proof that every trajectory assertion class can turn red. */
|
|
137
|
+
export declare function runTrajectoryMutationSelfTest(): MutationSelfTestReport;
|