@skill-harness/core 0.30.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.
- package/dist/authoring-preflight.d.ts +9 -1
- package/dist/authoring-preflight.js +57 -4
- package/dist/authoring.js +11 -2
- package/dist/calibration.d.ts +57 -0
- package/dist/calibration.js +200 -60
- package/dist/canary.d.ts +28 -26
- package/dist/canary.js +109 -92
- package/dist/diagnose.d.ts +2 -0
- package/dist/diagnose.js +12 -3
- package/dist/grade.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/journal.d.ts +3 -0
- package/dist/lint.js +2 -2
- package/dist/regate.js +1 -0
- package/dist/regrade.d.ts +3 -1
- package/dist/regrade.js +90 -73
- package/dist/rescore.js +1 -0
- package/dist/resource-monitor.d.ts +3 -0
- package/dist/resource-monitor.js +72 -0
- package/dist/results.d.ts +9 -4
- package/dist/results.js +4 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +128 -108
- package/dist/saved-run.d.ts +15 -0
- package/dist/saved-run.js +114 -0
- package/dist/scheduler.d.ts +3 -5
- package/dist/scheduler.js +18 -6
- package/dist/sources.js +13 -34
- package/dist/util/exec.d.ts +5 -0
- package/dist/util/exec.js +16 -32
- package/dist/util/fixture-inventory.d.ts +19 -0
- package/dist/util/fixture-inventory.js +68 -0
- package/dist/util/process.d.ts +78 -0
- package/dist/util/process.js +394 -0
- package/dist/workspace.js +13 -29
- package/package.json +1 -1
package/dist/sources.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync
|
|
3
|
-
import { isAbsolute,
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
5
|
import { resolveJudgeReferences } from "./judge-references.js";
|
|
6
6
|
import { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
|
|
7
|
+
import { fixtureInventory, hashFixtureFile } from "./util/fixture-inventory.js";
|
|
7
8
|
export { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
|
|
8
9
|
/**
|
|
9
10
|
* What a run measured, as a map of `key -> sha256`, recorded in results.yaml so
|
|
@@ -114,42 +115,20 @@ export function fileSha256(path) {
|
|
|
114
115
|
* normalised so a Linux-recorded hash still matches on Windows.
|
|
115
116
|
*/
|
|
116
117
|
export function dirSha256(dir) {
|
|
117
|
-
let files;
|
|
118
118
|
try {
|
|
119
|
-
files =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
h.update(rel);
|
|
127
|
-
h.update("\0");
|
|
128
|
-
try {
|
|
129
|
-
h.update(readFileSync(join(dir, rel)));
|
|
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");
|
|
130
126
|
}
|
|
131
|
-
|
|
132
|
-
return null; // a file that vanished mid-walk makes the whole digest untrustworthy
|
|
133
|
-
}
|
|
134
|
-
h.update("\0");
|
|
127
|
+
return h.digest("hex");
|
|
135
128
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/** Relative POSIX paths of every file under `dir`, recursively. Throws if `dir` is unreadable. */
|
|
139
|
-
function walk(dir, prefix = "") {
|
|
140
|
-
const out = [];
|
|
141
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
142
|
-
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
143
|
-
if (e.isDirectory()) {
|
|
144
|
-
out.push(...walk(join(dir, e.name), rel));
|
|
145
|
-
}
|
|
146
|
-
else if (e.isFile()) {
|
|
147
|
-
out.push(rel);
|
|
148
|
-
}
|
|
149
|
-
// symlinks/sockets/etc. are deliberately skipped: a fixture is plain files,
|
|
150
|
-
// and following links could hash something outside the fixture entirely.
|
|
129
|
+
catch {
|
|
130
|
+
return null; // Missing, oversized or changing input is unavailable evidence.
|
|
151
131
|
}
|
|
152
|
-
return out;
|
|
153
132
|
}
|
|
154
133
|
/**
|
|
155
134
|
* A scenario's semantic digest: everything that changes what the scenario
|
package/dist/util/exec.d.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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>;
|