@skill-harness/core 0.29.0 → 0.31.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 (59) hide show
  1. package/README.md +16 -2
  2. package/dist/adapters/types.d.ts +2 -0
  3. package/dist/authoring-preflight.d.ts +22 -0
  4. package/dist/authoring-preflight.js +111 -0
  5. package/dist/authoring.d.ts +96 -0
  6. package/dist/authoring.js +451 -0
  7. package/dist/calibration.d.ts +138 -0
  8. package/dist/calibration.js +352 -0
  9. package/dist/canary.d.ts +28 -26
  10. package/dist/canary.js +109 -92
  11. package/dist/command-cost.d.ts +1 -1
  12. package/dist/command-cost.js +2 -0
  13. package/dist/diagnose.d.ts +17 -0
  14. package/dist/diagnose.js +70 -0
  15. package/dist/execution-trace.d.ts +2 -1
  16. package/dist/execution-trace.js +4 -3
  17. package/dist/grade.d.ts +8 -0
  18. package/dist/grade.js +37 -8
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.js +8 -0
  21. package/dist/journal.d.ts +3 -0
  22. package/dist/judge-evidence.d.ts +40 -0
  23. package/dist/judge-evidence.js +120 -0
  24. package/dist/judge-references.d.ts +8 -0
  25. package/dist/judge-references.js +34 -0
  26. package/dist/lint.d.ts +1 -1
  27. package/dist/lint.js +22 -0
  28. package/dist/regate.js +21 -3
  29. package/dist/regrade.d.ts +10 -1
  30. package/dist/regrade.js +146 -75
  31. package/dist/rescore.js +1 -0
  32. package/dist/resource-monitor.d.ts +3 -0
  33. package/dist/resource-monitor.js +72 -0
  34. package/dist/results.d.ts +16 -4
  35. package/dist/results.js +10 -0
  36. package/dist/run.d.ts +2 -2
  37. package/dist/run.js +147 -116
  38. package/dist/saved-run.d.ts +15 -0
  39. package/dist/saved-run.js +114 -0
  40. package/dist/scheduler.d.ts +3 -5
  41. package/dist/scheduler.js +18 -6
  42. package/dist/seeded.js +1 -0
  43. package/dist/sources.d.ts +4 -1
  44. package/dist/sources.js +44 -40
  45. package/dist/spec.d.ts +8 -0
  46. package/dist/spec.js +27 -0
  47. package/dist/trace-gates.d.ts +2 -0
  48. package/dist/trace-gates.js +13 -3
  49. package/dist/util/exec.d.ts +5 -0
  50. package/dist/util/exec.js +16 -32
  51. package/dist/util/fixture-inventory.d.ts +19 -0
  52. package/dist/util/fixture-inventory.js +68 -0
  53. package/dist/util/process.d.ts +78 -0
  54. package/dist/util/process.js +394 -0
  55. package/dist/workspace.js +13 -29
  56. package/package.json +1 -1
  57. package/schemas/results-v2.schema.json +10 -0
  58. package/schemas/results-v3.schema.json +10 -0
  59. package/schemas/specification-v1.schema.json +21 -0
package/dist/sources.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync, readdirSync } from "node:fs";
3
- import { isAbsolute, join, resolve } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { isAbsolute, resolve } from "node:path";
4
4
  import yaml from "js-yaml";
5
+ import { resolveJudgeReferences } from "./judge-references.js";
5
6
  import { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
7
+ import { fixtureInventory, hashFixtureFile } from "./util/fixture-inventory.js";
6
8
  export { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
7
9
  /**
8
10
  * What a run measured, as a map of `key -> sha256`, recorded in results.yaml so
@@ -113,42 +115,20 @@ export function fileSha256(path) {
113
115
  * normalised so a Linux-recorded hash still matches on Windows.
114
116
  */
115
117
  export function dirSha256(dir) {
116
- let files;
117
118
  try {
118
- files = walk(dir).sort();
119
- }
120
- catch {
121
- return null;
122
- }
123
- const h = createHash("sha256");
124
- for (const rel of files) {
125
- h.update(rel);
126
- h.update("\0");
127
- try {
128
- h.update(readFileSync(join(dir, rel)));
129
- }
130
- catch {
131
- return null; // a file that vanished mid-walk makes the whole digest untrustworthy
119
+ const files = fixtureInventory(dir);
120
+ const h = createHash("sha256");
121
+ for (const file of files) {
122
+ h.update(file.relative);
123
+ h.update("\0");
124
+ hashFixtureFile(h, file);
125
+ h.update("\0");
132
126
  }
133
- h.update("\0");
127
+ return h.digest("hex");
134
128
  }
135
- return h.digest("hex");
136
- }
137
- /** Relative POSIX paths of every file under `dir`, recursively. Throws if `dir` is unreadable. */
138
- function walk(dir, prefix = "") {
139
- const out = [];
140
- for (const e of readdirSync(dir, { withFileTypes: true })) {
141
- const rel = prefix ? `${prefix}/${e.name}` : e.name;
142
- if (e.isDirectory()) {
143
- out.push(...walk(join(dir, e.name), rel));
144
- }
145
- else if (e.isFile()) {
146
- out.push(rel);
147
- }
148
- // symlinks/sockets/etc. are deliberately skipped: a fixture is plain files,
149
- // and following links could hash something outside the fixture entirely.
129
+ catch {
130
+ return null; // Missing, oversized or changing input is unavailable evidence.
150
131
  }
151
- return out;
152
132
  }
153
133
  /**
154
134
  * A scenario's semantic digest: everything that changes what the scenario
@@ -170,7 +150,7 @@ function walk(dir, prefix = "") {
170
150
  * transcripts are actually invalid.
171
151
  */
172
152
  function facets(s) {
173
- const { id, title, critical, mode, turns, checklist, fixture, assert, traceAssert, workspace, remote, systemPromptFile, extensions, reps, passThreshold, covers: _coversIsMetadata, ...restScenario } = s;
153
+ const { id, title, critical, mode, turns, checklist, judgeContext, judgeEvidence, judgeReferences, fixture, assert, traceAssert, workspace, remote, systemPromptFile, extensions, tools, reps, passThreshold, covers: _coversIsMetadata, ...restScenario } = s;
174
154
  const _scenarioExhaustive = restScenario;
175
155
  void _scenarioExhaustive;
176
156
  // `covers` is destructured into a discard on purpose, and this comment is the
@@ -206,8 +186,15 @@ function facets(s) {
206
186
  id, mode, turns, workspace, remote, systemPromptFile ?? null,
207
187
  fixture ?? null, vitest ?? null, post_test ?? null,
208
188
  ...(extensions ? [extensions] : []),
189
+ ...(tools !== undefined ? [["tools", tools]] : []),
190
+ ]),
191
+ // Judge-only reference text changes grading, not what the subject received.
192
+ // Preserve historical hashes when the optional field is absent.
193
+ rubric: JSON.stringify([id, title, checklist,
194
+ ...(judgeContext !== undefined ? [["judge_context", judgeContext]] : []),
195
+ ...(judgeEvidence !== undefined ? [["judge_evidence", judgeEvidence]] : []),
196
+ ...(judgeReferences !== undefined ? [["judge_references", judgeReferences]] : []),
209
197
  ]),
210
- rubric: JSON.stringify([id, title, checklist]),
211
198
  policy: JSON.stringify([id, critical, reps ?? null, passThreshold ?? null]),
212
199
  // Same rule as `stimulus` above: conditional, so a needle-gated scenario that
213
200
  // declares no trace assertions keeps the digest it was published with.
@@ -227,8 +214,15 @@ function sha(canonical) {
227
214
  export function stimulusDigest(s) {
228
215
  return sha(facets(s).stimulus);
229
216
  }
230
- export function rubricDigest(s) {
231
- return sha(facets(s).rubric);
217
+ export function rubricDigest(s, specDir, references) {
218
+ if (!s.judgeReferences?.length)
219
+ return sha(facets(s).rubric);
220
+ if (!references && !specDir)
221
+ throw new Error("A specification directory is required to digest selected judge references");
222
+ const selected = references ?? resolveJudgeReferences(s, specDir);
223
+ if (selected.length !== s.judgeReferences.length || selected.some((ref, index) => ref.file !== s.judgeReferences[index]))
224
+ throw new Error("Resolved judge references do not match the scenario selection");
225
+ return sha(JSON.stringify([facets(s).rubric, selected.map(({ file, sha256 }) => ({ file, sha256 }))]));
232
226
  }
233
227
  export function policyDigest(s) {
234
228
  return sha(facets(s).policy);
@@ -312,7 +306,12 @@ export function sourceHashes(ctx) {
312
306
  // single key could only ever name the most expensive one. The legacy
313
307
  // `scenario:<id>` key is deliberately NOT written any more — see scenarioDigest.
314
308
  hashes[STIMULUS_PREFIX + s.id] = stimulusDigest(s);
315
- hashes[RUBRIC_PREFIX + s.id] = rubricDigest(s);
309
+ try {
310
+ hashes[RUBRIC_PREFIX + s.id] = rubricDigest(s, ctx.specDir, ctx.judgeReferences?.[s.id]);
311
+ }
312
+ catch {
313
+ hashes[RUBRIC_PREFIX + s.id] = UNREADABLE;
314
+ }
316
315
  hashes[POLICY_PREFIX + s.id] = policyDigest(s);
317
316
  const gates = gatesDigest(s);
318
317
  if (gates !== null)
@@ -384,7 +383,12 @@ export function currentHashFor(key, ctx) {
384
383
  // scenario is a reshape, not staleness, on every kind.
385
384
  const facetResolvers = [
386
385
  [STIMULUS_PREFIX, stimulusDigest],
387
- [RUBRIC_PREFIX, rubricDigest],
386
+ [RUBRIC_PREFIX, s => { try {
387
+ return rubricDigest(s, ctx.specDir);
388
+ }
389
+ catch {
390
+ return null;
391
+ } }],
388
392
  [POLICY_PREFIX, policyDigest],
389
393
  [GATES_PREFIX, gatesDigest],
390
394
  [SCENARIO_PREFIX, scenarioDigest],
package/dist/spec.d.ts CHANGED
@@ -31,6 +31,12 @@ export interface Scenario {
31
31
  mode: ScenarioMode;
32
32
  turns: string[];
33
33
  checklist: string[];
34
+ /** Explicit author-supplied reference text for the judge only; never a subject prompt or file path. */
35
+ judgeContext?: string;
36
+ /** Judge evidence is independent of whether a fixture workspace is used. */
37
+ judgeEvidence?: "response" | "code";
38
+ /** Explicit judge-only files, relative to the specification directory. */
39
+ judgeReferences?: string[];
34
40
  fixture?: string;
35
41
  assert?: ScenarioAssert;
36
42
  /**
@@ -54,6 +60,8 @@ export interface Scenario {
54
60
  * silently become part of the test.
55
61
  */
56
62
  extensions?: string[];
63
+ /** Explicit Pi tool filter; an empty list disables tools. */
64
+ tools?: string[];
57
65
  /**
58
66
  * `covers`: instruction sections this scenario is declared to exercise, e.g.
59
67
  * `SKILL.md#core-principle`.
package/dist/spec.js CHANGED
@@ -191,6 +191,33 @@ export function parseSpec(text, file) {
191
191
  workspace: "none",
192
192
  remote: false,
193
193
  };
194
+ const envTools = s.env && typeof s.env === "object" ? s.env.tools : undefined;
195
+ if (envTools !== undefined) {
196
+ if (!Array.isArray(envTools) || envTools.some(tool => typeof tool !== "string" || !/^[A-Za-z][A-Za-z0-9_:/.-]*$/.test(tool)) || new Set(envTools).size !== envTools.length) {
197
+ throw new SpecError(`scenario \`${id}\` env.tools must be a list of unique Pi tool names`, file);
198
+ }
199
+ scenario.tools = envTools;
200
+ }
201
+ if (s.judge_context !== undefined) {
202
+ if (typeof s.judge_context !== "string" || s.judge_context.trim() === "" || Buffer.byteLength(s.judge_context, "utf8") > 65_536) {
203
+ throw new SpecError(`scenario \`${id}\` \`judge_context\` must be a non-empty string of at most 65536 UTF-8 bytes`, file);
204
+ }
205
+ scenario.judgeContext = s.judge_context;
206
+ }
207
+ if (s.judge_evidence !== undefined) {
208
+ if (s.judge_evidence !== "response" && s.judge_evidence !== "code") {
209
+ throw new SpecError(`scenario \`${id}\` \`judge_evidence\` must be response or code`, file);
210
+ }
211
+ scenario.judgeEvidence = s.judge_evidence;
212
+ }
213
+ if (s.judge_references !== undefined) {
214
+ assertStringList(s.judge_references, id, "judge_references", file);
215
+ if (s.judge_references.length > 16 || new Set(s.judge_references).size !== s.judge_references.length ||
216
+ s.judge_references.some(path => !path || /[\\\\:\x00-\x1f]/.test(path) || path.startsWith("/") || path.split("/").some(part => !part || part === "." || part === ".."))) {
217
+ throw new SpecError(`scenario \`${id}\` \`judge_references\` requires at most 16 unique relative POSIX file paths without traversal`, file);
218
+ }
219
+ scenario.judgeReferences = s.judge_references;
220
+ }
194
221
  // Output and trace assertions are legal for inline AND seeded scenarios.
195
222
  if (s.assert !== undefined && (s.assert === null || typeof s.assert !== "object" || Array.isArray(s.assert))) {
196
223
  throw new SpecError(`scenario \`${id}\` \`assert\` must be a mapping`, file);
@@ -33,6 +33,8 @@ export interface RequireCall {
33
33
  tool: string;
34
34
  count?: CountConstraint;
35
35
  args?: Record<string, ArgPredicate>;
36
+ /** Optional tool-reported result; omission preserves invocation-only matching. */
37
+ outcome?: "success" | "error";
36
38
  }
37
39
  export interface ForbidCall {
38
40
  tool: string;
@@ -43,10 +43,15 @@ export function evaluateTraceGates(assert, trace) {
43
43
  assertions.push({ kind: "trace_evidence", status: "ERROR", detail: error });
44
44
  }
45
45
  for (const req of assert.require_calls ?? []) {
46
- const matched = trace.tool_calls.filter((c) => c.name === req.tool && argsMatch(c, req.args));
46
+ const candidates = trace.tool_calls.filter((c) => c.name === req.tool && argsMatch(c, req.args));
47
+ if (req.outcome && candidates.some(c => !Number.isInteger(c.completionIndex) || c.completionIndex < 0 || typeof c.isError !== "boolean")) {
48
+ assertions.push({ kind: "require_call", status: "ERROR", detail: `cannot establish ${req.outcome} outcome for \`${req.tool}\`: completion evidence is missing` });
49
+ continue;
50
+ }
51
+ const matched = req.outcome ? candidates.filter(c => c.isError === (req.outcome === "error")) : candidates;
47
52
  const min = req.count?.min ?? 1;
48
53
  const max = req.count?.max;
49
- const described = describeArgs(req.args);
54
+ const described = describeArgs(req.args) + (req.outcome ? ` with tool-reported ${req.outcome}` : "");
50
55
  if (matched.length < min) {
51
56
  assertions.push({
52
57
  kind: "require_call",
@@ -354,8 +359,13 @@ export function parseTraceAssert(raw, ctx) {
354
359
  req.count = parseCount(entry.count, `${ctx}: \`require_calls[${i}].count\``);
355
360
  if (entry.args !== undefined)
356
361
  req.args = parseArgs(entry.args, `${ctx}: \`require_calls[${i}].args\``);
362
+ if (entry.outcome !== undefined) {
363
+ if (entry.outcome !== "success" && entry.outcome !== "error")
364
+ throw new Error(`${ctx}: \`require_calls[${i}].outcome\` must be success or error`);
365
+ req.outcome = entry.outcome;
366
+ }
357
367
  for (const key of Object.keys(entry)) {
358
- if (!["tool", "count", "args"].includes(key)) {
368
+ if (!["tool", "count", "args", "outcome"].includes(key)) {
359
369
  throw new Error(`${ctx}: unknown key \`${key}\` in \`require_calls[${i}]\``);
360
370
  }
361
371
  }
@@ -1,12 +1,17 @@
1
+ import { type ProcessObservation } from "./process.js";
1
2
  export interface ExecResult {
2
3
  stdout: string;
3
4
  stderr: string;
4
5
  code: number | null;
6
+ observation?: ProcessObservation;
5
7
  }
6
8
  export interface ExecOpts {
7
9
  cwd?: string;
8
10
  timeoutMs?: number;
9
11
  env?: NodeJS.ProcessEnv;
12
+ signal?: AbortSignal;
13
+ maxStdoutBytes?: number;
14
+ maxStderrBytes?: number;
10
15
  }
11
16
  /** Spawn a command, capture stdout/stderr. Never throws on non-zero exit; returns the code. */
12
17
  export declare function exec(cmd: string, args: string[], opts?: ExecOpts): Promise<ExecResult>;
package/dist/util/exec.js CHANGED
@@ -1,38 +1,22 @@
1
- import { spawn } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
2
  import { join, delimiter } from "node:path";
3
+ import { BoundedCapture, ProcessOutputLimitError, runProcess } from "./process.js";
4
4
  /** Spawn a command, capture stdout/stderr. Never throws on non-zero exit; returns the code. */
5
- export function exec(cmd, args, opts = {}) {
6
- return new Promise((resolve, reject) => {
7
- // stdin must be ignored (not inherited): pi blocks reading stdin even in
8
- // --print mode if its stdin is an open pipe/tty.
9
- const child = spawn(cmd, args, {
10
- cwd: opts.cwd,
11
- env: opts.env ?? process.env,
12
- stdio: ["ignore", "pipe", "pipe"],
13
- });
14
- let stdout = "";
15
- let stderr = "";
16
- let timer;
17
- if (opts.timeoutMs) {
18
- timer = setTimeout(() => {
19
- child.kill("SIGKILL");
20
- stderr += `\n[skill-harness] killed after ${opts.timeoutMs}ms timeout`;
21
- }, opts.timeoutMs);
22
- }
23
- child.stdout.on("data", (d) => (stdout += d.toString()));
24
- child.stderr.on("data", (d) => (stderr += d.toString()));
25
- child.on("error", (e) => {
26
- if (timer)
27
- clearTimeout(timer);
28
- reject(e);
29
- });
30
- child.on("close", (code) => {
31
- if (timer)
32
- clearTimeout(timer);
33
- resolve({ stdout, stderr, code });
34
- });
35
- });
5
+ export async function exec(cmd, args, opts = {}) {
6
+ const stdout = new BoundedCapture(opts.maxStdoutBytes ?? 8 * 1024 * 1024);
7
+ const stderr = new BoundedCapture(opts.maxStderrBytes ?? 128 * 1024);
8
+ const capture = (sink, name) => (chunk) => {
9
+ sink.push(chunk);
10
+ if (sink.truncated)
11
+ throw new ProcessOutputLimitError(`${name} capture exceeded ${sink.limit} bytes`);
12
+ };
13
+ const result = await runProcess(cmd, args, { ...opts,
14
+ onStdout: capture(stdout, "stdout"), onStderr: capture(stderr, "stderr") });
15
+ // A partial command output must never masquerade as a successful judge answer,
16
+ // git diff, or tool lookup. Ordinary non-zero exits remain the caller's decision.
17
+ if (result.error)
18
+ throw result.error;
19
+ return { stdout: stdout.text(), stderr: stderr.text(), code: result.code, observation: result.observation };
36
20
  }
37
21
  /** True if a binary is resolvable on PATH. */
38
22
  export function onPath(bin) {
@@ -0,0 +1,19 @@
1
+ import type { Hash } from "node:crypto";
2
+ export declare const FIXTURE_LIMITS: Readonly<{
3
+ entries: 4096;
4
+ depth: 32;
5
+ fileBytes: number;
6
+ totalBytes: number;
7
+ }>;
8
+ export declare class FixtureLimitError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export interface FixtureFile {
12
+ path: string;
13
+ relative: string;
14
+ size: number;
15
+ }
16
+ /** Bounded metadata walk of one explicitly selected tree. Never follows child symlinks. */
17
+ export declare function fixtureInventory(root: string, skip?: (name: string, relative: string) => boolean): FixtureFile[];
18
+ /** Update the existing hash format without allocating a whole file or accepting a growing file. */
19
+ export declare function hashFixtureFile(hash: Hash, file: FixtureFile): void;
@@ -0,0 +1,68 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export const FIXTURE_LIMITS = Object.freeze({ entries: 4096, depth: 32, fileBytes: 64 * 1024 * 1024, totalBytes: 256 * 1024 * 1024 });
4
+ export class FixtureLimitError extends Error {
5
+ constructor(message) { super(`Selected fixture limit exceeded: ${message}`); this.name = "FixtureLimitError"; }
6
+ }
7
+ /** Bounded metadata walk of one explicitly selected tree. Never follows child symlinks. */
8
+ export function fixtureInventory(root, skip) {
9
+ const files = [];
10
+ let entries = 0, total = 0;
11
+ const walk = (dir, prefix, depth) => {
12
+ if (depth > FIXTURE_LIMITS.depth)
13
+ throw new FixtureLimitError(`depth exceeds ${FIXTURE_LIMITS.depth}`);
14
+ const opened = opendirSync(dir);
15
+ try {
16
+ for (let entry = opened.readSync(); entry; entry = opened.readSync()) {
17
+ if (++entries > FIXTURE_LIMITS.entries)
18
+ throw new FixtureLimitError(`more than ${FIXTURE_LIMITS.entries} entries`);
19
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
20
+ if (skip?.(entry.name, relative))
21
+ continue;
22
+ const path = join(dir, entry.name);
23
+ if (entry.isDirectory())
24
+ walk(path, relative, depth + 1);
25
+ else if (entry.isFile()) {
26
+ const stat = lstatSync(path);
27
+ if (!stat.isFile())
28
+ throw new Error(`Selected fixture changed during inventory: ${relative}`);
29
+ if (stat.size > FIXTURE_LIMITS.fileBytes)
30
+ throw new FixtureLimitError(`${relative} exceeds ${FIXTURE_LIMITS.fileBytes} bytes`);
31
+ total += stat.size;
32
+ if (total > FIXTURE_LIMITS.totalBytes)
33
+ throw new FixtureLimitError(`total exceeds ${FIXTURE_LIMITS.totalBytes} bytes`);
34
+ files.push({ path, relative, size: stat.size });
35
+ }
36
+ }
37
+ }
38
+ finally {
39
+ opened.closeSync();
40
+ }
41
+ };
42
+ walk(root, "", 0);
43
+ return files.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
44
+ }
45
+ /** Update the existing hash format without allocating a whole file or accepting a growing file. */
46
+ export function hashFixtureFile(hash, file) {
47
+ const fd = openSync(file.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
48
+ try {
49
+ const stat = fstatSync(fd);
50
+ if (!stat.isFile() || stat.size !== file.size || stat.size > FIXTURE_LIMITS.fileBytes)
51
+ throw new Error(`Selected fixture changed before hashing: ${file.relative}`);
52
+ const buffer = Buffer.allocUnsafe(64 * 1024);
53
+ let offset = 0;
54
+ while (offset < file.size) {
55
+ const count = readSync(fd, buffer, 0, Math.min(buffer.length, file.size - offset), offset);
56
+ if (!count)
57
+ throw new Error(`Selected fixture changed while hashing: ${file.relative}`);
58
+ hash.update(buffer.subarray(0, count));
59
+ offset += count;
60
+ }
61
+ if (readSync(fd, buffer, 0, 1, offset) !== 0 || fstatSync(fd).size !== file.size)
62
+ throw new Error(`Selected fixture changed while hashing: ${file.relative}`);
63
+ }
64
+ finally {
65
+ closeSync(fd);
66
+ }
67
+ }
68
+ //# sourceMappingURL=fixture-inventory.js.map
@@ -0,0 +1,78 @@
1
+ export interface ProcessObservation {
2
+ schema: 1;
3
+ platform: string;
4
+ scope: "owned-process-group" | "child-process";
5
+ pid: number | null;
6
+ reason: "running" | "exited" | "timeout" | "aborted" | "output-limit" | "resource-limit" | "spawn-error";
7
+ code: number | null;
8
+ durationMs: number;
9
+ stdoutBytes: number;
10
+ stderrBytes: number;
11
+ samples: number;
12
+ processesObserved: number;
13
+ peakRssBytes: number | null;
14
+ cpuUserTicks: number | null;
15
+ cpuSystemTicks: number | null;
16
+ readBytes: number | null;
17
+ writeBytes: number | null;
18
+ gaps: string[];
19
+ cleanup: {
20
+ termSent: boolean;
21
+ killSent: boolean;
22
+ groupGone: boolean | null;
23
+ pipesClosed: boolean;
24
+ };
25
+ }
26
+ export interface ProcessScope {
27
+ signal?: AbortSignal;
28
+ /** One metadata-only record on settlement; never receives argv, environment or output. */
29
+ onObservation?: (record: ProcessObservation) => void;
30
+ /** Latest active metadata, initially and at most every five seconds; never raw logs. */
31
+ onSample?: (record: ProcessObservation) => void;
32
+ /** Optional owned-tree sampled limits; these are not host quotas or sandbox boundaries. */
33
+ maxRssBytes?: number;
34
+ maxWriteBytes?: number;
35
+ }
36
+ /** Scope all subprocesses in an evaluation, including concurrent subject/judge calls. */
37
+ export declare function withProcessScope<T>(scope: ProcessScope, work: () => T): T;
38
+ export declare class ProcessControlError extends Error {
39
+ readonly reason: "aborted" | "output-limit" | "resource-limit";
40
+ constructor(reason: "aborted" | "output-limit" | "resource-limit", message: string);
41
+ }
42
+ export declare class ProcessOutputLimitError extends ProcessControlError {
43
+ constructor(message: string);
44
+ }
45
+ export declare function isProcessControlError(error: unknown): error is ProcessControlError;
46
+ export declare function getProcessSignal(): AbortSignal | undefined;
47
+ export declare function throwIfProcessAborted(): void;
48
+ /** Retains at most `limit` bytes, including a single oversized chunk. */
49
+ export declare class BoundedCapture {
50
+ readonly limit: number;
51
+ private chunks;
52
+ private tailUsed;
53
+ bytes: number;
54
+ truncated: boolean;
55
+ constructor(limit: number);
56
+ push(chunk: Buffer): void;
57
+ text(): string;
58
+ }
59
+ export interface ProcessOptions {
60
+ cwd?: string;
61
+ env?: NodeJS.ProcessEnv;
62
+ timeoutMs?: number;
63
+ signal?: AbortSignal;
64
+ /** Total stream traffic ceiling, independent of how much a consumer retains. */
65
+ maxOutputBytes?: number;
66
+ onStdout?: (chunk: Buffer) => void;
67
+ onStderr?: (chunk: Buffer) => void;
68
+ onStdoutEnd?: () => void;
69
+ /** Internal/test seam; not a model execution budget. */
70
+ killGraceMs?: number;
71
+ }
72
+ export interface ProcessResult {
73
+ code: number | null;
74
+ observation: ProcessObservation;
75
+ error: Error | null;
76
+ }
77
+ /** Shared owned-process lifecycle. Callers retain only their bounded stream representation. */
78
+ export declare function runProcess(command: string, args: string[], options?: ProcessOptions): Promise<ProcessResult>;