praxis-agent 0.46.4 → 0.47.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.
@@ -0,0 +1,395 @@
1
+ import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
2
+ import { isAbsolute, join, resolve, sep } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { Minimatch, minimatch } from 'minimatch';
5
+ export const PROJECT_EVAL_SCHEMA_VERSION = '1.0';
6
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
7
+ const MAX_ITEMS = 256;
8
+ const MAX_STRING = 16 * 1024;
9
+ const MAX_BYTES = 1024 * 1024;
10
+ const DEFAULT_PROJECT_EVAL_ALLOWED_TOOLS = ['Read', 'Glob', 'Grep'];
11
+ function validateBounds(value, label = 'case', depth = 0, state = { nodes: 0 }) {
12
+ state.nodes += 1;
13
+ if (state.nodes > 4096)
14
+ throw new Error(`${label} exceeds object node limit`);
15
+ if (depth > 16)
16
+ throw new Error(`${label} exceeds object depth limit`);
17
+ if (typeof value === 'string' && value.length > MAX_STRING)
18
+ throw new Error(`${label} contains oversized string`);
19
+ if (Array.isArray(value)) {
20
+ if (value.length > MAX_ITEMS)
21
+ throw new Error(`${label} contains oversized array`);
22
+ for (const item of value)
23
+ validateBounds(item, label, depth + 1, state);
24
+ }
25
+ else if (value && typeof value === 'object') {
26
+ const entries = Object.entries(value);
27
+ if (entries.length > MAX_ITEMS)
28
+ throw new Error(`${label} contains oversized object`);
29
+ for (const [key, item] of entries) {
30
+ if (key.length > 256)
31
+ throw new Error(`${label} contains oversized key`);
32
+ validateBounds(item, label, depth + 1, state);
33
+ }
34
+ }
35
+ }
36
+ function obj(v, label) {
37
+ if (!v || typeof v !== 'object' || Array.isArray(v))
38
+ throw new Error(`${label} must be an object`);
39
+ return v;
40
+ }
41
+ function strict(v, keys, label) {
42
+ const bad = Object.keys(v).find((k) => !keys.includes(k));
43
+ if (bad)
44
+ throw new Error(`${label} has unknown field: ${bad}`);
45
+ }
46
+ function str(v, label, required = true) {
47
+ if (v === undefined && !required)
48
+ return '';
49
+ if (typeof v !== 'string' || !v.trim() || v.length > MAX_STRING)
50
+ throw new Error(`${label} must be a non-empty string`);
51
+ return v;
52
+ }
53
+ function path(v, label) {
54
+ const s = str(v, label);
55
+ const p = s.replaceAll('\\', '/');
56
+ if (isAbsolute(s) ||
57
+ s.includes('\0') ||
58
+ p.split('/').some((x) => !x || x === '..' || x === '.') ||
59
+ /^[A-Za-z]:\//u.test(p))
60
+ throw new Error(`${label} must be a contained relative path`);
61
+ return p;
62
+ }
63
+ function list(v, label) {
64
+ if (v === undefined)
65
+ return [];
66
+ if (!Array.isArray(v) ||
67
+ v.length > MAX_ITEMS ||
68
+ v.some((x) => typeof x !== 'string' || !x || x.length > MAX_STRING))
69
+ throw new Error(`${label} must be a bounded string array`);
70
+ return v.map((x) => String(x));
71
+ }
72
+ function integer(v, label, fallback, min, max) {
73
+ const n = v ?? fallback;
74
+ if (!Number.isInteger(n) || Number(n) < min || Number(n) > max)
75
+ throw new Error(`${label} must be an integer from ${min} to ${max}`);
76
+ return Number(n);
77
+ }
78
+ function glob(v, label) {
79
+ const s = path(v, label);
80
+ if (!hasBalancedGlobDelimiters(s))
81
+ throw new Error(`${label} is invalid`);
82
+ try {
83
+ const matcher = new Minimatch(s, { nonegate: true, nocomment: true });
84
+ if (!matcher.makeRe())
85
+ throw new Error('invalid');
86
+ }
87
+ catch {
88
+ throw new Error(`${label} is invalid`);
89
+ }
90
+ return s;
91
+ }
92
+ function focus(v, label, fallback) {
93
+ if (v === undefined)
94
+ return fallback;
95
+ if (v === 'last_message' || v === 'trace' || v === 'files')
96
+ return v;
97
+ const item = obj(v, label);
98
+ strict(item, ['source', 'path'], label);
99
+ if (item.source !== 'file')
100
+ throw new Error(`${label}.source must be file`);
101
+ return { source: 'file', path: path(item.path, `${label}.path`) };
102
+ }
103
+ function toolMatch(v, label) {
104
+ if (typeof v === 'string' && v.length > 0)
105
+ return str(v, label);
106
+ const item = obj(v, label);
107
+ strict(item, ['tool', 'input_match'], label);
108
+ if (item.input_match !== undefined)
109
+ validateBounds(item.input_match, label);
110
+ return {
111
+ tool: str(item.tool, `${label}.tool`),
112
+ ...(item.input_match === undefined
113
+ ? {}
114
+ : { input_match: obj(item.input_match, `${label}.input_match`) }),
115
+ };
116
+ }
117
+ function projectGrader(v, index) {
118
+ const item = obj(v, `graders[${index}]`);
119
+ const name = str(item.name, `graders[${index}].name`);
120
+ if (!NAME.test(name))
121
+ throw new Error(`graders[${index}].name must be a safe eval identifier`);
122
+ if (item.arm !== undefined)
123
+ throw new Error(`${name}.arm is not supported in project eval graders`);
124
+ const type = str(item.type, `${name}.type`);
125
+ const weight = item.weight === undefined ? 1 : Number(item.weight);
126
+ if (!(weight > 0) || !Number.isFinite(weight))
127
+ throw new Error(`${name}.weight must be positive`);
128
+ const base = { name, weight };
129
+ if (type === 'regex') {
130
+ strict(item, ['type', 'name', 'weight', 'target', 'pattern', 'flags', 'match'], name);
131
+ const match = item.match === undefined ? 'contains' : String(item.match);
132
+ if (!['contains', 'not_contains'].includes(match) &&
133
+ !/^count:\d+$/u.test(match))
134
+ throw new Error(`${name}.match is invalid`);
135
+ const flags = item.flags === undefined ? '' : String(item.flags);
136
+ const pattern = str(item.pattern, `${name}.pattern`);
137
+ try {
138
+ new RegExp(pattern, flags);
139
+ }
140
+ catch {
141
+ throw new Error(`${name}.pattern or flags are invalid`);
142
+ }
143
+ return {
144
+ ...base,
145
+ type: 'regex',
146
+ target: focus(item.target, `${name}.target`, 'last_message'),
147
+ pattern,
148
+ flags,
149
+ match: match,
150
+ };
151
+ }
152
+ if (type === 'tool_order') {
153
+ strict(item, ['type', 'name', 'weight', 'before', 'after'], name);
154
+ return {
155
+ ...base,
156
+ type: 'tool_order',
157
+ before: toolMatch(item.before, `${name}.before`),
158
+ after: toolMatch(item.after, `${name}.after`),
159
+ };
160
+ }
161
+ if (type === 'tool_used') {
162
+ strict(item, ['type', 'name', 'weight', 'tool', 'input_match', 'min', 'max'], name);
163
+ const min = integer(item.min, `${name}.min`, 1, 0, Number.MAX_SAFE_INTEGER);
164
+ const max = item.max === undefined
165
+ ? Number.POSITIVE_INFINITY
166
+ : integer(item.max, `${name}.max`, 0, 0, Number.MAX_SAFE_INTEGER);
167
+ if (max < min)
168
+ throw new Error(`${name}.max must be at least min`);
169
+ return {
170
+ ...base,
171
+ type: 'tool_used',
172
+ tool: str(item.tool, `${name}.tool`),
173
+ ...(item.input_match === undefined
174
+ ? {}
175
+ : { input_match: obj(item.input_match, `${name}.input_match`) }),
176
+ min,
177
+ max,
178
+ };
179
+ }
180
+ if (type === 'file_exists') {
181
+ strict(item, ['type', 'name', 'weight', 'path', 'exists'], name);
182
+ if (item.exists !== undefined && typeof item.exists !== 'boolean')
183
+ throw new Error(`${name}.exists must be boolean`);
184
+ return {
185
+ ...base,
186
+ type: 'file_exists',
187
+ path: glob(item.path, `${name}.path`),
188
+ exists: item.exists !== false,
189
+ };
190
+ }
191
+ if (type === 'llm' || type === 'baseline')
192
+ throw new Error(`Project eval grader ${name} does not support paid type ${type}`);
193
+ throw new Error(`Unsupported project grader type: ${type}`);
194
+ }
195
+ function hasBalancedGlobDelimiters(value) {
196
+ const depths = { '[': 0, '{': 0, '(': 0 };
197
+ const opening = {
198
+ ']': '[',
199
+ '}': '{',
200
+ ')': '(',
201
+ };
202
+ for (const character of value) {
203
+ if (character === '[' || character === '{' || character === '(') {
204
+ depths[character] += 1;
205
+ continue;
206
+ }
207
+ const opener = opening[character];
208
+ if (opener !== undefined && --depths[opener] < 0)
209
+ return false;
210
+ }
211
+ return Object.values(depths).every((depth) => depth === 0);
212
+ }
213
+ export function parseProjectEvalCase(raw, dir) {
214
+ const r = obj(raw, 'case');
215
+ validateBounds(r);
216
+ strict(r, [
217
+ 'schema_version',
218
+ 'name',
219
+ 'tags',
220
+ 'runs',
221
+ 'fixture',
222
+ 'execution',
223
+ 'verification',
224
+ 'graders',
225
+ 'expect',
226
+ ], 'case');
227
+ if (r.schema_version !== '1.0')
228
+ throw new Error(`Unsupported schema_version: ${String(r.schema_version)}`);
229
+ const name = str(r.name, 'name');
230
+ if (!NAME.test(name))
231
+ throw new Error('name must be a safe eval identifier');
232
+ const ex = obj(r.execution, 'execution');
233
+ strict(ex, [
234
+ 'prompt',
235
+ 'max_turns',
236
+ 'timeout_seconds',
237
+ 'allowed_tools',
238
+ 'model',
239
+ 'append_system_prompt',
240
+ 'env',
241
+ ], 'execution');
242
+ const expect = obj(r.expect, 'expect');
243
+ strict(expect, [
244
+ 'allowed_changed_paths',
245
+ 'expected_changed_paths',
246
+ 'forbidden_changed_paths',
247
+ ], 'expect');
248
+ const allowed = list(expect.allowed_changed_paths, 'expect.allowed_changed_paths').map((x) => glob(x, 'expect.allowed_changed_paths'));
249
+ const fixture = path(r.fixture, 'fixture');
250
+ const env = obj(ex.env ?? {}, 'execution.env');
251
+ for (const [k, v] of Object.entries(env))
252
+ if (!/^EVAL_[A-Z0-9_]+$/u.test(k) || typeof v !== 'string')
253
+ throw new Error(`Invalid eval environment variable: ${k}`);
254
+ const vr = r.verification === undefined ? [] : r.verification;
255
+ if (!Array.isArray(vr) || vr.length > MAX_ITEMS)
256
+ throw new Error('verification must be a bounded array');
257
+ const verification = vr.map((x, i) => {
258
+ const q = obj(x, `verification[${i}]`);
259
+ strict(q, ['name', 'command', 'args', 'timeout_seconds'], `verification[${i}]`);
260
+ return {
261
+ name: str(q.name, `verification[${i}].name`),
262
+ command: str(q.command, `verification[${i}].command`),
263
+ args: list(q.args, `verification[${i}].args`),
264
+ timeoutSeconds: integer(q.timeout_seconds, `verification[${i}].timeout_seconds`, 120, 1, 3600),
265
+ };
266
+ });
267
+ const gradersRaw = r.graders === undefined ? [] : r.graders;
268
+ if (!Array.isArray(gradersRaw) || gradersRaw.length > MAX_ITEMS)
269
+ throw new Error('graders must be a bounded array');
270
+ const graders = gradersRaw.map(projectGrader);
271
+ if (new Set(graders.map((item) => item.name)).size !== graders.length)
272
+ throw new Error('Project grader names must be unique');
273
+ return {
274
+ schemaVersion: '1.0',
275
+ name,
276
+ dir,
277
+ fixture,
278
+ tags: list(r.tags, 'tags'),
279
+ runs: integer(r.runs, 'runs', 1, 1, 50),
280
+ execution: {
281
+ prompt: str(ex.prompt, 'execution.prompt'),
282
+ maxTurns: integer(ex.max_turns, 'execution.max_turns', 10, 1, 200),
283
+ timeoutSeconds: integer(ex.timeout_seconds, 'execution.timeout_seconds', 120, 1, 3600),
284
+ allowedTools: list(ex.allowed_tools, 'execution.allowed_tools').length
285
+ ? list(ex.allowed_tools, 'execution.allowed_tools')
286
+ : [...DEFAULT_PROJECT_EVAL_ALLOWED_TOOLS],
287
+ ...(ex.model === undefined
288
+ ? {}
289
+ : { model: str(ex.model, 'execution.model') }),
290
+ ...(ex.append_system_prompt === undefined
291
+ ? {}
292
+ : {
293
+ appendSystemPrompt: str(ex.append_system_prompt, 'execution.append_system_prompt'),
294
+ }),
295
+ env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
296
+ },
297
+ verification,
298
+ graders,
299
+ expect: {
300
+ allowedChangedPaths: allowed,
301
+ expectedChangedPaths: list(expect.expected_changed_paths, 'expect.expected_changed_paths').map((x) => glob(x, 'expect.expected_changed_paths')),
302
+ forbiddenChangedPaths: list(expect.forbidden_changed_paths, 'expect.forbidden_changed_paths').map((x) => glob(x, 'expect.forbidden_changed_paths')),
303
+ },
304
+ };
305
+ }
306
+ export async function loadProjectEvalCase(dir) {
307
+ const root = await realpath(resolve(dir));
308
+ const definition = join(root, 'case.yaml');
309
+ const definitionStat = await lstat(definition);
310
+ if (definitionStat.isSymbolicLink() || !definitionStat.isFile())
311
+ throw new Error('case.yaml must be a regular file');
312
+ if (definitionStat.size > MAX_BYTES)
313
+ throw new Error('case.yaml exceeds 1 MiB');
314
+ const content = await readFile(definition, 'utf8');
315
+ if (Buffer.byteLength(content) > MAX_BYTES)
316
+ throw new Error('case.yaml exceeds 1 MiB');
317
+ const raw = parseYaml(content, { maxAliasCount: 20 });
318
+ validateBounds(raw);
319
+ const c = parseProjectEvalCase(raw, root);
320
+ const fixturePath = resolve(root, c.fixture);
321
+ const fixtureStat = await lstat(fixturePath);
322
+ if (fixtureStat.isSymbolicLink() || !fixtureStat.isDirectory())
323
+ throw new Error('fixture must be a regular directory');
324
+ const fixture = await realpath(fixturePath);
325
+ if (fixture !== root && !fixture.startsWith(`${root}${sep}`))
326
+ throw new Error('fixture escapes case directory');
327
+ return { ...c, fixture };
328
+ }
329
+ export async function discoverProjectEvalCases(target, filter, tags = []) {
330
+ if (filter !== undefined) {
331
+ if (!filter ||
332
+ filter.includes('\\') ||
333
+ filter.includes('/') ||
334
+ filter.includes('\0') ||
335
+ filter.length > MAX_STRING ||
336
+ /^[A-Za-z]:/u.test(filter) ||
337
+ !hasBalancedGlobDelimiters(filter))
338
+ throw new Error('Invalid case glob');
339
+ try {
340
+ if (!new Minimatch(filter, { nonegate: true, nocomment: true }).makeRe())
341
+ throw new Error('invalid');
342
+ }
343
+ catch {
344
+ throw new Error('Invalid case glob');
345
+ }
346
+ }
347
+ const root = await realpath(resolve(target));
348
+ const discovered = [];
349
+ async function walk(d, depth = 0) {
350
+ if (depth > 16)
351
+ throw new Error('eval discovery exceeds depth 16');
352
+ const entries = (await readdir(d, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
353
+ const inspected = await Promise.all(entries.map(async (entry) => {
354
+ const path = join(d, entry.name);
355
+ const entryStat = await lstat(path);
356
+ if (entryStat.isSymbolicLink())
357
+ throw new Error(`Eval definition tree contains symlink: ${path}`);
358
+ if (!entryStat.isFile() && !entryStat.isDirectory())
359
+ throw new Error(`Unsupported eval definition entry: ${path}`);
360
+ return { entry, entryStat, path };
361
+ }));
362
+ const definition = inspected.find(({ entry }) => entry.name === 'case.yaml');
363
+ if (definition) {
364
+ if (!definition.entryStat.isFile())
365
+ throw new Error('case.yaml must be a regular file');
366
+ const parsed = await loadProjectEvalCase(d);
367
+ discovered.push(parsed);
368
+ return;
369
+ }
370
+ for (const { entry, entryStat, path } of inspected) {
371
+ if (entry.name === 'results' ||
372
+ entry.name === '.git' ||
373
+ entry.name === 'node_modules' ||
374
+ entry.name === '.claude')
375
+ continue;
376
+ if (entryStat.isDirectory())
377
+ await walk(path, depth + 1);
378
+ }
379
+ }
380
+ const evalRoot = join(root, 'evals');
381
+ const evalRootStat = await lstat(evalRoot).catch(() => null);
382
+ if (!evalRootStat ||
383
+ evalRootStat.isSymbolicLink() ||
384
+ !evalRootStat.isDirectory())
385
+ throw new Error(`No evals directory in ${root}`);
386
+ await walk(evalRoot);
387
+ if (new Set(discovered.map((c) => c.name)).size !== discovered.length)
388
+ throw new Error('Project eval case names must be unique');
389
+ const out = discovered.filter((parsed) => (!filter || minimatch(parsed.name, filter)) &&
390
+ (!tags.length || tags.some((tag) => parsed.tags.includes(tag))));
391
+ if (!out.length)
392
+ throw new Error('No selected project eval cases');
393
+ return out;
394
+ }
395
+ //# sourceMappingURL=project-eval-schema.js.map
@@ -0,0 +1,30 @@
1
+ export interface FileManifest {
2
+ files: Record<string, {
3
+ hash: string;
4
+ size: number;
5
+ mode: number;
6
+ }>;
7
+ totalBytes: number;
8
+ }
9
+ export declare const MAX_FILES = 10000;
10
+ export declare const MAX_BYTES: number;
11
+ declare function manifest(root: string): Promise<FileManifest>;
12
+ export declare function createProjectEvalWorkspace(fixture: string): Promise<{
13
+ root: string;
14
+ cwd: string;
15
+ config: string;
16
+ home: string;
17
+ out: string;
18
+ before: FileManifest;
19
+ sourceBefore: FileManifest;
20
+ manifest: typeof manifest;
21
+ }>;
22
+ export declare function diffProjectEvalWorkspace(before: FileManifest, after: FileManifest): Promise<{
23
+ added: string[];
24
+ modified: string[];
25
+ deleted: string[];
26
+ changed: string[];
27
+ }>;
28
+ export declare function cleanupProjectEvalWorkspace(root: string): Promise<void>;
29
+ export {};
30
+ //# sourceMappingURL=project-eval-workspace.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { chmod, copyFile, mkdtemp, mkdir, readdir, lstat, rm, } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { basename, join, relative } from 'node:path';
6
+ export const MAX_FILES = 10_000;
7
+ export const MAX_BYTES = 512 * 1024 * 1024;
8
+ async function hashFile(path) {
9
+ const h = createHash('sha256');
10
+ for await (const chunk of createReadStream(path))
11
+ h.update(chunk);
12
+ return h.digest('hex');
13
+ }
14
+ async function manifest(root) {
15
+ const files = Object.create(null);
16
+ let fileCount = 0;
17
+ let totalBytes = 0;
18
+ async function walk(dir) {
19
+ for (const e of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
20
+ const p = join(dir, e.name), s = await lstat(p);
21
+ if (s.isSymbolicLink() || (!s.isFile() && !s.isDirectory()))
22
+ throw new Error(`Unsupported fixture entry: ${relative(root, p)}`);
23
+ if (e.name === '.git' || e.name === 'node_modules')
24
+ throw new Error(`Fixture contains forbidden directory: ${relative(root, p)}`);
25
+ if (s.isDirectory())
26
+ await walk(p);
27
+ else {
28
+ if (fileCount >= MAX_FILES || totalBytes + s.size > MAX_BYTES)
29
+ throw new Error('Fixture exceeds manifest limits');
30
+ totalBytes += s.size;
31
+ fileCount += 1;
32
+ files[relative(root, p).replaceAll('\\', '/')] = {
33
+ hash: await hashFile(p),
34
+ size: s.size,
35
+ mode: s.mode & 0o777,
36
+ };
37
+ }
38
+ }
39
+ }
40
+ await walk(root);
41
+ return { files, totalBytes };
42
+ }
43
+ async function copyTree(source, target) {
44
+ for (const e of (await readdir(source, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
45
+ const from = join(source, e.name), to = join(target, e.name), s = await lstat(from);
46
+ if (s.isSymbolicLink() || (!s.isFile() && !s.isDirectory()))
47
+ throw new Error(`Unsupported fixture entry: ${relative(source, from)}`);
48
+ if (e.name === '.git' || e.name === 'node_modules')
49
+ throw new Error(`Fixture contains forbidden directory: ${relative(source, from)}`);
50
+ if (s.isDirectory()) {
51
+ await mkdir(to);
52
+ await copyTree(from, to);
53
+ }
54
+ else {
55
+ await copyFile(from, to);
56
+ await chmod(to, s.mode & 0o777);
57
+ }
58
+ }
59
+ }
60
+ export async function createProjectEvalWorkspace(fixture) {
61
+ const fixtureStat = await lstat(fixture);
62
+ if (fixtureStat.isSymbolicLink() || !fixtureStat.isDirectory())
63
+ throw new Error('Fixture root must be a directory');
64
+ if (basename(fixture) === '.git' || basename(fixture) === 'node_modules')
65
+ throw new Error('Fixture root is forbidden');
66
+ const root = await mkdtemp(join(tmpdir(), 'praxis-project-eval-'));
67
+ try {
68
+ const cwd = join(root, 'cwd'), config = join(root, 'config'), home = join(root, 'home'), out = join(root, 'out');
69
+ await Promise.all([mkdir(cwd), mkdir(config), mkdir(home), mkdir(out)]);
70
+ const sourceBefore = await manifest(fixture);
71
+ await copyTree(fixture, cwd);
72
+ const before = await manifest(cwd);
73
+ const sourceAfter = await manifest(fixture);
74
+ if (JSON.stringify(sourceBefore) !== JSON.stringify(sourceAfter) ||
75
+ JSON.stringify(sourceBefore) !== JSON.stringify(before))
76
+ throw new Error('Fixture changed while creating workspace');
77
+ return { root, cwd, config, home, out, before, sourceBefore, manifest };
78
+ }
79
+ catch (e) {
80
+ await rm(root, { recursive: true, force: true });
81
+ throw e;
82
+ }
83
+ }
84
+ export async function diffProjectEvalWorkspace(before, after) {
85
+ const added = [], modified = [], deleted = [];
86
+ for (const p of Object.keys(after.files)) {
87
+ if (!Object.hasOwn(before.files, p))
88
+ added.push(p);
89
+ else if (JSON.stringify(before.files[p]) !== JSON.stringify(after.files[p]))
90
+ modified.push(p);
91
+ }
92
+ for (const p of Object.keys(before.files))
93
+ if (!Object.hasOwn(after.files, p))
94
+ deleted.push(p);
95
+ added.sort();
96
+ modified.sort();
97
+ deleted.sort();
98
+ return {
99
+ added,
100
+ modified,
101
+ deleted,
102
+ changed: [...added, ...modified, ...deleted].sort(),
103
+ };
104
+ }
105
+ export async function cleanupProjectEvalWorkspace(root) {
106
+ await rm(root, { recursive: true, force: true });
107
+ }
108
+ //# sourceMappingURL=project-eval-workspace.js.map
@@ -0,0 +1,77 @@
1
+ import type { ModelUsage } from '../core/runtime.js';
2
+ import type { EvalRuntimeFactory } from './eval-contract.js';
3
+ import { type ProjectEvalRunResult } from './project-eval-runner.js';
4
+ export declare const PROJECT_EVAL_HELP = "Usage: praxis eval [options] <target>\n\nRun deterministic project outcome evaluations in isolated workspaces.\n\nOptions:\n --case <glob> Filter case names\n --tag <tag[,tag]> Filter tags; repeatable\n --runs <1..50> Override run count\n --model <model> Override model\n --allow-tools <rules> Grant gated tools; comma-separated and repeatable\n --run-verification Enable verifier subprocesses\n --output-dir <dir> Write artifacts to this directory\n --keep-temp Preserve temporary workspaces\n --json Print exactly one aggregate JSON value\n --verbose Print run progress to stderr\n -h, --help Display help";
5
+ export interface ProjectEvalDependencies {
6
+ runtimeFactory: EvalRuntimeFactory;
7
+ version?: string;
8
+ configRoot: string;
9
+ }
10
+ export interface ProjectEvalOptions {
11
+ target?: string;
12
+ caseGlob?: string;
13
+ tags: string[];
14
+ runs?: number;
15
+ model?: string;
16
+ allowTools: string[];
17
+ runVerification: boolean;
18
+ outputDir?: string;
19
+ keepTemp: boolean;
20
+ json: boolean;
21
+ verbose: boolean;
22
+ help?: true;
23
+ }
24
+ export interface ProjectEvalUsageTotals {
25
+ input_tokens: number;
26
+ output_tokens: number;
27
+ cache_read_input_tokens: number;
28
+ cache_creation_input_tokens: number;
29
+ web_search_requests: number;
30
+ }
31
+ export interface ProjectEvalRunSummary {
32
+ case: string;
33
+ run: number;
34
+ model: string | null;
35
+ passed: boolean;
36
+ score: 0 | 1;
37
+ turns: number;
38
+ usage: ModelUsage | null;
39
+ cost_usd: number | null;
40
+ cost_known: boolean;
41
+ duration_ms: number;
42
+ termination: ProjectEvalRunResult['termination'];
43
+ error: string | null;
44
+ artifact_dir: string;
45
+ }
46
+ export interface ProjectEvalAggregate {
47
+ schema_version: '1.0';
48
+ version: string;
49
+ start: string;
50
+ duration_ms: number;
51
+ target: string;
52
+ output_dir: string;
53
+ model: string | null;
54
+ case_count: number;
55
+ planned_run_count: number;
56
+ completed_run_count: number;
57
+ run_count: number;
58
+ passed: number;
59
+ failed: number;
60
+ pass_rate: number;
61
+ total_turns: number;
62
+ usage_totals: ProjectEvalUsageTotals;
63
+ usage_known_runs: number;
64
+ usage_unknown_runs: number;
65
+ known_cost_total_usd: number | null;
66
+ known_cost_runs: number;
67
+ unknown_cost_runs: number;
68
+ partial: boolean;
69
+ interrupted: boolean;
70
+ runs: readonly ProjectEvalRunSummary[];
71
+ }
72
+ export declare function parseProjectEvalOptions(argv: readonly string[]): ProjectEvalOptions;
73
+ export declare function executeProjectEvalCommand(argv: readonly string[], io: {
74
+ stdout(message: string): void;
75
+ stderr(message: string): void;
76
+ }, dependencies: ProjectEvalDependencies, callerCwd?: string, signal?: AbortSignal): Promise<number>;
77
+ //# sourceMappingURL=project-eval.d.ts.map