@skill-harness/core 0.8.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 +3 -0
- package/dist/index.js +3 -0
- package/dist/lint.js +6 -0
- 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/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.js +11 -3
- package/dist/spec.d.ts +15 -0
- package/dist/spec.js +53 -1
- 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/seeded.js
CHANGED
|
@@ -168,12 +168,15 @@ export async function runSeeded(scenario, opts) {
|
|
|
168
168
|
cwd: repo,
|
|
169
169
|
// Resolved against the spec dir, exactly like fixtures and post-tests.
|
|
170
170
|
extensions: scenario.extensions?.map((e) => resolve(opts.specDir, e)),
|
|
171
|
+
eventSources: scenario.eventSources,
|
|
171
172
|
};
|
|
172
173
|
// A trace-gated seeded scenario runs through the structured path so the tool
|
|
173
174
|
// calls are recorded; everything downstream (gates, diff, transcript) is
|
|
174
175
|
// identical, because the rebuilt transcript is what print mode would have
|
|
175
176
|
// emitted anyway.
|
|
176
177
|
let traces = [];
|
|
178
|
+
let events = [];
|
|
179
|
+
let eventErrors = [];
|
|
177
180
|
let harnessOut;
|
|
178
181
|
if (opts.trace) {
|
|
179
182
|
if (!opts.adapter.runStructured) {
|
|
@@ -187,12 +190,15 @@ export async function runSeeded(scenario, opts) {
|
|
|
187
190
|
});
|
|
188
191
|
harnessOut = structured.transcript;
|
|
189
192
|
traces = structured.traces;
|
|
193
|
+
events = structured.events ?? [];
|
|
194
|
+
eventErrors = structured.eventErrors ?? [];
|
|
190
195
|
}
|
|
191
196
|
else {
|
|
192
197
|
harnessOut = await opts.adapter.run(req);
|
|
193
198
|
}
|
|
194
199
|
const parts = [harnessOut, "", "=== SEEDED GATES ==="];
|
|
195
200
|
let gateFailure = null;
|
|
201
|
+
let gateError = null;
|
|
196
202
|
const runVitest = opts.runVitest ??
|
|
197
203
|
((args, cwd) => exec("npx", ["vitest", "run", ...args], { cwd, timeoutMs: VITEST_TIMEOUT_MS }));
|
|
198
204
|
// `exec` never throws on a non-zero exit and, on timeout, SIGKILLs the child and
|
|
@@ -213,7 +219,8 @@ export async function runSeeded(scenario, opts) {
|
|
|
213
219
|
(gitFailure.stderr.trim() ? `: ${gitFailure.stderr.trim().split("\n")[0]}` : "");
|
|
214
220
|
parts.push(` staged diff: ERROR (${msg})`);
|
|
215
221
|
gateFailure = msg;
|
|
216
|
-
|
|
222
|
+
gateError = msg;
|
|
223
|
+
return finish(parts, gateFailure, gateError, diff, traces, events, eventErrors);
|
|
217
224
|
}
|
|
218
225
|
// BOTH needle gates read the changed lines only, never context. A unified diff
|
|
219
226
|
// carries three lines of context per hunk, so an untouched symbol near the edit
|
|
@@ -241,10 +248,14 @@ export async function runSeeded(scenario, opts) {
|
|
|
241
248
|
? ` vitest run: ERROR (timed out after ${VITEST_TIMEOUT_MS}ms — infrastructure, not model behavior)`
|
|
242
249
|
: ` vitest run: ${passed ? "PASS" : `FAIL (exit ${v.code})`}`);
|
|
243
250
|
parts.push(indent(bothStreams(v)));
|
|
244
|
-
if (!passed
|
|
245
|
-
|
|
251
|
+
if (!passed) {
|
|
252
|
+
const problem = killed
|
|
246
253
|
? `vitest timed out after ${VITEST_TIMEOUT_MS}ms — infrastructure, not model behavior`
|
|
247
254
|
: `vitest failed (exit ${v.code})`;
|
|
255
|
+
if (!gateFailure)
|
|
256
|
+
gateFailure = problem;
|
|
257
|
+
if (killed)
|
|
258
|
+
gateError = problem;
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
const postTest = scenario.assert?.post_test;
|
|
@@ -263,6 +274,7 @@ export async function runSeeded(scenario, opts) {
|
|
|
263
274
|
parts.push(` post_test: ERROR (${msg})`);
|
|
264
275
|
if (!gateFailure)
|
|
265
276
|
gateFailure = msg;
|
|
277
|
+
gateError = msg;
|
|
266
278
|
}
|
|
267
279
|
else {
|
|
268
280
|
// `.test` is part of the name we build, never taken from the source: extname()
|
|
@@ -281,7 +293,8 @@ export async function runSeeded(scenario, opts) {
|
|
|
281
293
|
parts.push(` post_test: ERROR (${msg})`);
|
|
282
294
|
if (!gateFailure)
|
|
283
295
|
gateFailure = msg;
|
|
284
|
-
|
|
296
|
+
gateError = msg;
|
|
297
|
+
return finish(parts, gateFailure, gateError, diff, traces, events, eventErrors);
|
|
285
298
|
}
|
|
286
299
|
const v = await runVitest([POST_TEST_BASE], repo);
|
|
287
300
|
const out = `${v.stdout}\n${v.stderr}`;
|
|
@@ -300,13 +313,17 @@ export async function runSeeded(scenario, opts) {
|
|
|
300
313
|
const notCollected = /^\s*No test files found/im.test(out);
|
|
301
314
|
const killed = v.code === null;
|
|
302
315
|
let problem = null;
|
|
316
|
+
let problemIsError = false;
|
|
303
317
|
if (killed) {
|
|
318
|
+
problemIsError = true;
|
|
304
319
|
problem = `post_test ${JSON.stringify(postTest)} timed out after ${VITEST_TIMEOUT_MS}ms — infrastructure, not model behavior`;
|
|
305
320
|
}
|
|
306
321
|
else if (notCollected) {
|
|
322
|
+
problemIsError = true;
|
|
307
323
|
problem = `post_test ${JSON.stringify(postTest)} was never collected by vitest — spec/fixture error, not model behavior`;
|
|
308
324
|
}
|
|
309
325
|
else if (tally === null) {
|
|
326
|
+
problemIsError = true;
|
|
310
327
|
problem = `post_test ${JSON.stringify(postTest)} produced no parseable vitest summary (exit ${v.code}) — cannot confirm it ran`;
|
|
311
328
|
}
|
|
312
329
|
else if (v.code !== 0 || tally.failed > 0) {
|
|
@@ -317,20 +334,24 @@ export async function runSeeded(scenario, opts) {
|
|
|
317
334
|
problem = `post_test ${JSON.stringify(postTest)} failed (exit ${v.code})`;
|
|
318
335
|
}
|
|
319
336
|
else if (tally.skipped > 0 || tally.todo > 0) {
|
|
337
|
+
problemIsError = true;
|
|
320
338
|
problem = `post_test ${JSON.stringify(postTest)} has ${tally.skipped + tally.todo} skipped/todo test(s) — a hidden gate must actually run; spec error, not model behavior`;
|
|
321
339
|
}
|
|
322
340
|
else if (tally.passed === 0) {
|
|
341
|
+
problemIsError = true;
|
|
323
342
|
problem = `post_test ${JSON.stringify(postTest)} ran no assertions — spec error, not model behavior`;
|
|
324
343
|
}
|
|
325
344
|
parts.push(problem === null
|
|
326
345
|
? ` post_test ${JSON.stringify(postTest)}: PASS (${tally.passed} assertion-bearing test(s))`
|
|
327
|
-
: ` post_test ${JSON.stringify(postTest)}: ${
|
|
346
|
+
: ` post_test ${JSON.stringify(postTest)}: ${problemIsError ? "ERROR" : "FAIL"} (${problem})`);
|
|
328
347
|
parts.push(indent(bothStreams(v)));
|
|
329
348
|
if (problem && !gateFailure)
|
|
330
349
|
gateFailure = problem;
|
|
350
|
+
if (problem && problemIsError)
|
|
351
|
+
gateError = problem;
|
|
331
352
|
}
|
|
332
353
|
}
|
|
333
|
-
return finish(parts, gateFailure, diff, traces);
|
|
354
|
+
return finish(parts, gateFailure, gateError, diff, traces, events, eventErrors);
|
|
334
355
|
}
|
|
335
356
|
function git(cwd, args) {
|
|
336
357
|
return exec("git", args, { cwd, timeoutMs: 30_000 });
|
|
@@ -364,13 +385,13 @@ function bothStreams(v) {
|
|
|
364
385
|
return o || e;
|
|
365
386
|
}
|
|
366
387
|
/** Append the staged diff and return the outcome. Every exit path goes through here, so the judge always sees the same sections in the same order. */
|
|
367
|
-
function finish(parts, gateFailure, diff, traces = []) {
|
|
388
|
+
function finish(parts, gateFailure, gateError, diff, traces = [], events = [], eventErrors = []) {
|
|
368
389
|
// The code itself, last — the gates above only prove that keywords appeared.
|
|
369
390
|
// Without this section a seeded checklist item about what the code *does* is
|
|
370
391
|
// graded from the model's own description of its work.
|
|
371
392
|
parts.push("", "=== STAGED DIFF ===");
|
|
372
393
|
parts.push(diff.trim() === "" ? " (empty — the model left no staged changes)" : capDiff(diff));
|
|
373
|
-
return { transcript: parts.join("\n"), gateFailure, diff, traces };
|
|
394
|
+
return { transcript: parts.join("\n"), gateFailure, gateError, diff, traces, events, eventErrors };
|
|
374
395
|
}
|
|
375
396
|
/**
|
|
376
397
|
* Parse vitest's `Tests N passed | M skipped (T)` summary line.
|
package/dist/sources.js
CHANGED
|
@@ -168,7 +168,7 @@ function walk(dir, prefix = "") {
|
|
|
168
168
|
* transcripts are actually invalid.
|
|
169
169
|
*/
|
|
170
170
|
function facets(s) {
|
|
171
|
-
const { id, title, critical, mode, turns, checklist, fixture, assert, traceAssert, workspace, remote, systemPromptFile, extensions, reps, passThreshold, covers: _coversIsMetadata, ...restScenario } = s;
|
|
171
|
+
const { id, title, critical, mode, turns, checklist, fixture, assert, traceAssert, trajectoryAssert, workspace, remote, systemPromptFile, extensions, eventSources, reps, passThreshold, covers: _coversIsMetadata, ...restScenario } = s;
|
|
172
172
|
const _scenarioExhaustive = restScenario;
|
|
173
173
|
void _scenarioExhaustive;
|
|
174
174
|
// `covers` is destructured into a discard on purpose, and this comment is the
|
|
@@ -185,7 +185,7 @@ function facets(s) {
|
|
|
185
185
|
// already saved, so `regate` (free) can re-answer it without re-running the model.
|
|
186
186
|
// Note the asymmetry with `env.extensions` in Phase 3, which IS stimulus — one
|
|
187
187
|
// changes what gets executed, the other only what we conclude from it.
|
|
188
|
-
const hasGates = diff_contains !== undefined || diff_excludes !== undefined || traceAssert !== undefined;
|
|
188
|
+
const hasGates = diff_contains !== undefined || diff_excludes !== undefined || traceAssert !== undefined || trajectoryAssert !== undefined;
|
|
189
189
|
return {
|
|
190
190
|
// `vitest` and the `post_test` PATH are stimulus, not gates: both change what the
|
|
191
191
|
// run executes in the workspace, and neither can be re-evaluated from a saved
|
|
@@ -204,13 +204,21 @@ function facets(s) {
|
|
|
204
204
|
id, mode, turns, workspace, remote, systemPromptFile ?? null,
|
|
205
205
|
fixture ?? null, vitest ?? null, post_test ?? null,
|
|
206
206
|
...(extensions ? [extensions] : []),
|
|
207
|
+
// Event-source paths choose which native ledgers are captured. A saved
|
|
208
|
+
// normalized artifact cannot answer for a source that was never collected,
|
|
209
|
+
// so changing this is stimulus and needs a re-run.
|
|
210
|
+
...(eventSources ? [eventSources] : []),
|
|
207
211
|
]),
|
|
208
212
|
rubric: JSON.stringify([id, title, checklist]),
|
|
209
213
|
policy: JSON.stringify([id, critical, reps ?? null, passThreshold ?? null]),
|
|
210
214
|
// Same rule as `stimulus` above: conditional, so a needle-gated scenario that
|
|
211
215
|
// declares no trace assertions keeps the digest it was published with.
|
|
212
216
|
gates: hasGates
|
|
213
|
-
? JSON.stringify([
|
|
217
|
+
? JSON.stringify([
|
|
218
|
+
id, diff_contains ?? null, diff_excludes ?? null,
|
|
219
|
+
...(traceAssert ? [traceAssert] : []),
|
|
220
|
+
...(trajectoryAssert ? [trajectoryAssert] : []),
|
|
221
|
+
])
|
|
214
222
|
: null,
|
|
215
223
|
};
|
|
216
224
|
}
|
package/dist/spec.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceKind } from "./workspace.js";
|
|
2
2
|
import { type TraceAssert } from "./trace-gates.js";
|
|
3
|
+
import { type TrajectoryAssert } from "./trajectory-gates.js";
|
|
3
4
|
export type ScenarioMode = "inline" | "seeded";
|
|
4
5
|
export interface SeededAssert {
|
|
5
6
|
vitest?: boolean;
|
|
@@ -20,6 +21,14 @@ export interface SeededAssert {
|
|
|
20
21
|
*/
|
|
21
22
|
post_test?: string;
|
|
22
23
|
}
|
|
24
|
+
export interface TrajectoryEventSource {
|
|
25
|
+
/** Native format adapter registered by the harness adapter. */
|
|
26
|
+
adapter: "normalized-v1" | "principal-assurance-v1" | "pi-daddy-v1";
|
|
27
|
+
/** Workspace-relative file/glob. Never an absolute path or traversal. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Missing evidence is an error by default; false is for optional supplemental ledgers. */
|
|
30
|
+
required: boolean;
|
|
31
|
+
}
|
|
23
32
|
export interface Scenario {
|
|
24
33
|
id: string;
|
|
25
34
|
title: string;
|
|
@@ -37,6 +46,10 @@ export interface Scenario {
|
|
|
37
46
|
* Declaring it opts the scenario into structured (`--mode json`) execution.
|
|
38
47
|
*/
|
|
39
48
|
traceAssert?: TraceAssert;
|
|
49
|
+
/** Objective assertions over adapter-neutral, versioned workflow events. */
|
|
50
|
+
trajectoryAssert?: TrajectoryAssert;
|
|
51
|
+
/** Native event/ledger files the harness normalizes after execution. */
|
|
52
|
+
eventSources?: TrajectoryEventSource[];
|
|
40
53
|
workspace: WorkspaceKind;
|
|
41
54
|
remote: boolean;
|
|
42
55
|
systemPromptFile?: string;
|
|
@@ -69,6 +82,8 @@ export interface ShipBar {
|
|
|
69
82
|
no_critical_fail: boolean;
|
|
70
83
|
}
|
|
71
84
|
export interface Spec {
|
|
85
|
+
/** Specification schema. Omitted YAML is read as v1 for 0.8 compatibility. */
|
|
86
|
+
schema?: 1;
|
|
72
87
|
skill: string;
|
|
73
88
|
judge_persona: string;
|
|
74
89
|
ship_bar: ShipBar;
|
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/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;
|