Setting the file. One moment.
Evals · Foundry Agentic Loop · github/awesome-copilot · Skills Docs
ContentsBack to the top of the page 180.10
Foundry Toolbox
138
export function exitCode
— line 138
This file
Number 180.24
Position 24 of 54
Type JavaScript
Size 22 KB
Lines 391 scripts/ evals.mjs
JavaScript · 391 lines · 22 KB
const
SKILL_DIR
=
path.
resolve
(path.
dirname
(
fileURLToPath
(
import
.
meta
.url)),
".."
);
10 const SKILL_NAME = "foundry-agentic-loop" ;
11 export const MODES = [ "rapid" , "sdd" , "improve" , "shared" ];
12 export const RISKS = [ "low" , "medium" , "high" ];
13 const TAG = / ^ [a-z0-9] + (?:- [a-z0-9] + ) *$ / ;
14 // Evals never reach Azure or the network: no shell, URLs, MCP servers or workflow tools, and files only inside the workspace.
15 export const AGENT_TOOLS = [ "view" , "glob" , "grep" , "skill" , "create" , "edit" ];
16 const LOCKDOWN = [ "--deny-tool" , "shell" , "--deny-tool" , "url" , "--disable-builtin-mcps" , "--disallow-temp-dir" , "--no-ask-user" , "-s" ];
17 const PREAMBLE = "[Eval harness] This is an offline evaluation workspace. Shell, network, Azure, workflow and extension tools are unavailable by design. "
18 + "Where a step needs one, state the exact tool call or command you would run and why, then continue. Never claim that it ran. "
19 + `Load the ${ SKILL_NAME } skill when the task is in its scope. \n\n ` ;
20 const AUTH = /not (?:logged | signed) in | no authentication | authentication (?:failed | required) | copilot login | unauthori [sz] ed |\b 401 \b / i ;
21 const MAX_TEXT = 40_000 ;
22 const USAGE = `Usage:
23 node evals.mjs index [--mode m] [--target file] [--capability c] [--ids 1,2]
24 node evals.mjs validate
25 node evals.mjs run (--ids 1,2 | --target file | --mode m | --capability c) [--base <git-ref|dir>] [--skill <dir>]
26 [--out <dir>] [--model m] [--grader-model m] [--runner <cmd>] [--concurrency n]
27 [--timeout <seconds>] [--max-ai-credits n] [--evals <evals.json>]
28 Exit codes: 0 all pass (with --base: no regressions and every new or changed case passes), 1 failures, 2 evals unavailable.` ;
29
30 export function parseArgs ( argv ) {
31 const [ command , ... rest ] = argv;
32 const opts = {};
33 for ( let i = 0 ; i < rest. length ; i += 1 ) {
34 const match = / ^ --( [a-z-] + )(?:=( . * )) ?$ / s . exec (rest[i]);
35 if ( ! match) throw new Error ( `Unexpected argument: ${ rest [ i ] }` );
36 const value = match[ 2 ] ?? rest[ ++ i];
37 if (value === undefined ) throw new Error ( `--${ match [ 1 ] } needs a value.` );
38 opts[match[ 1 ]. replace ( /-( [a-z] )/ g , ( _ , c ) => c. toUpperCase ())] = value;
39 }
40 return { command, opts };
41 }
42
43 export async function loadCases ( file ) {
44 return JSON . parse ( await readFile (file, "utf8" )).evals;
45 }
46
47 const list = value => String (value ?? "" ). split ( "," ). map ( item => item. trim ()). filter (Boolean);
48 const matchesTarget = ( targets , value ) => targets. some ( target => target === value || target. endsWith ( `/${ value }` ));
49
50 export function filterCases ( cases , { ids , mode , target , capability } = {}) {
51 const wanted = new Set ( list (ids). map (Number));
52 return cases. filter ( c => ( ! wanted.size || wanted. has (c.id))
53 && ( ! mode || c.mode === mode)
54 && ( ! target || matchesTarget (c.targets ?? [], target))
55 && ( ! capability || (c.capabilities ?? []). includes (capability)));
56 }
57
58 export function indexLines ( cases ) {
59 const short = text => {
60 const line = String (text). replace ( / \s + / g , " " ). trim ();
61 return line. length > 80 ? `${ line . slice ( 0 , 79 ) }…` : line;
62 };
63 return cases. map ( c => [c.id, c.mode, c.risk, c.capabilities. join ( "," ), c.targets. join ( "," ), short (c.prompt)]. join ( " | " ));
64 }
65
66 export function validateCases ( cases , { evalsDir , skillDir }) {
67 const errors = [];
68 const ids = new Set ();
69 for ( const c of cases) {
70 const at = `case ${ c ?. id }` ;
71 if ( ! Number. isInteger (c?.id) || ids. has (c.id)) errors. push ( `${ at }: id must be a unique integer` );
72 ids. add (c?.id);
73 for ( const key of [ "prompt" , "expected_output" ]) if ( typeof c?.[key] !== "string" || ! c[key]. trim ()) errors. push ( `${ at }: ${ key } is required` );
74 if ( ! Array. isArray (c?.expectations) || ! c.expectations. length || c.expectations. some ( e => typeof e !== "string" || ! e. trim ())) errors. push ( `${ at }: expectations must be non-empty strings` );
75 if ( ! Array. isArray (c?.files)) errors. push ( `${ at }: files must be an array` );
76 if ( ! MODES . includes (c?.mode)) errors. push ( `${ at }: mode must be one of ${ MODES . join ( ", " ) }` );
77 if ( ! RISKS . includes (c?.risk)) errors. push ( `${ at }: risk must be one of ${ RISKS . join ( ", " ) }` );
78 if ( ! Array. isArray (c?.capabilities) || ! c.capabilities. length || c.capabilities. some ( tag => ! TAG . test (tag))) errors. push ( `${ at }: capabilities must be kebab-case tags` );
79 if ( ! Array. isArray (c?.targets) || ! c.targets. length ) errors. push ( `${ at }: targets must list skill files` );
80 else for ( const target of c.targets) if ( ! existsSync (path. join (skillDir, target))) errors. push ( `${ at }: target ${ target } does not exist` );
81 if (c?.fixture !== undefined ) {
82 const dir = path. join (evalsDir, "fixtures" , String (c.fixture));
83 if ( ! TAG . test ( String (c.fixture)) || ! existsSync (dir)) errors. push ( `${ at }: fixture ${ c . fixture } does not exist` );
84 else for ( const file of c.files ?? []) if ( ! existsSync (path. join (dir, file))) errors. push ( `${ at }: file ${ file } is not in fixture ${ c . fixture }` );
85 } else if (c?.files?. length ) errors. push ( `${ at }: files need a fixture` );
86 }
87 return errors;
88 }
89
90 // Returns the first JSON object embedded in the text (code fences and prose around it are ignored), or null.
91 export function extractJson ( text ) {
92 const s = String (text ?? "" );
93 for ( let start = s. indexOf ( "{" ); start !== - 1 ; start = s. indexOf ( "{" , start + 1 )) {
94 let depth = 0 ;
95 let quoted = false ;
96 let escaped = false ;
97 for ( let i = start; i < s. length ; i += 1 ) {
98 const ch = s[i];
99 if (quoted) {
100 if (escaped) escaped = false ;
101 else if (ch === " \\ " ) escaped = true ;
102 else if (ch === " \" " ) quoted = false ;
103 } else if (ch === " \" " ) quoted = true ;
104 else if (ch === "{" ) depth += 1 ;
105 else if (ch === "}" && -- depth === 0 ) {
106 try {
107 const value = JSON . parse (s. slice (start, i + 1 ));
108 if (value && typeof value === "object" && ! Array. isArray (value)) return value;
109 } catch { /* try the next opening brace */ }
110 break ;
111 }
112 }
113 }
114 return null ;
115 }
116
117 export function grade ( c , text ) {
118 const rows = extractJson (text)?.expectations;
119 if ( ! Array. isArray (rows) || rows. length !== c.expectations. length || rows. some ( row => typeof row?.passed !== "boolean" )) {
120 return { status: "error" , error: "The grader returned no valid JSON verdict." , expectations: [] };
121 }
122 const expectations = c.expectations. map (( expectation , i ) => ({ text: expectation, passed: rows[i].passed, evidence: String (rows[i].evidence ?? "" ). slice ( 0 , 500 ) }));
123 return { status: expectations. every ( row => row.passed) ? "passed" : "failed" , expectations };
124 }
125
126 export const signature = c => JSON . stringify ([c.prompt, c.expected_output, c.expectations, c.fixture ?? null ]);
127
128 // New or changed cases must pass outright; a failing unchanged case is a regression only when it passed at the base.
129 export function classify ( current , base , changed ) {
130 if (current.status === "error" ) return "error" ;
131 if (current.status === "passed" ) return "passed" ;
132 if (changed) return "new-failing" ;
133 if ( ! base) return "failed" ;
134 if (base.status === "error" ) return "error" ;
135 return base.status === "passed" ? "regression" : "pre-existing" ;
136 }
137
138 export function exitCode ( results ) {
139 if (results. some ( r => r.unavailable)) return 2 ;
140 return results. every ( r => [ "passed" , "pre-existing" ]. includes (r.verdict)) ? 0 : 1 ;
141 }
142
143 function exec ( command , args , { cwd , timeoutMs = 0 } = {}) {
144 return new Promise ( resolve => {
145 const child = spawn (command[ 0 ], [ ... command. slice ( 1 ), ... args], { cwd, stdio: [ "ignore" , "pipe" , "pipe" ] });
146 let stdout = "" ;
147 let stderr = "" ;
148 let timedOut = false ;
149 const timer = timeoutMs ? setTimeout (() => { timedOut = true ; child. kill ( "SIGTERM" ); }, timeoutMs) : null ;
150 child.stdout. on ( "data" , chunk => { stdout += chunk; });
151 child.stderr. on ( "data" , chunk => { stderr += chunk; });
152 child. on ( "error" , error => { clearTimeout (timer); resolve ({ code: null , stdout, stderr, error }); });
153 child. on ( "close" , code => { clearTimeout (timer); resolve ({ code, stdout, stderr, timedOut }); });
154 });
155 }
156
157 const unavailable = r => r.error?.code === "ENOENT" || (r.code !== 0 && AUTH . test ( `${ r . stderr } \n ${ r . stdout }` ));
158 const failure = r => (r.timedOut ? "timed out" : r.error?.message ?? `exit ${ r . code }: ${ ( r . stderr || r . stdout ). trim (). slice ( - 500 ) }` );
159
160 async function git ( cwd , args ) {
161 const r = await exec ([ "git" ], [ "-c" , "user.name=evals" , "-c" , "user.email=evals@example.invalid" , "-c" , "commit.gpgsign=false" , "-c" , "core.fsmonitor=false" , "-c" , "gc.auto=0" , ... args], { cwd });
162 if (r.code !== 0 ) throw new Error ( `git ${ args [ 0 ] } failed: ${ failure ( r ) }` );
163 return r.stdout;
164 }
165
166 // A fresh Git workspace with the case fixture and the candidate skill installed as a project override, minus its evals.
167 export async function prepareWorkspace ( ws , c , { evalsDir , skillDir }) {
168 await mkdir (ws, { recursive: true });
169 if (c.fixture) await cp (path. join (evalsDir, "fixtures" , c.fixture), ws, { recursive: true });
170 // Fixtures store .gitignore as "gitignore" so the repository still tracks the files it lists, such as a placeholder .env.
171 if ( existsSync (path. join (ws, "gitignore" ))) await rename (path. join (ws, "gitignore" ), path. join (ws, ".gitignore" ));
172 await cp (skillDir, path. join (ws, ".github" , "skills" , SKILL_NAME ), {
173 recursive: true ,
174 filter : src => path. relative (skillDir, src). split (path.sep)[ 0 ] !== "evals" ,
175 });
176 await git (ws, [ "init" , "-q" ]);
177 await git (ws, [ "add" , "-A" ]);
178 await git (ws, [ "commit" , "-q" , "--no-verify" , "-m" , "eval fixture" ]);
179 }
180
181 async function changedFiles ( ws ) {
182 const out = await git (ws, [ "status" , "--porcelain" , "--untracked-files=all" ]);
183 return out. split ( " \n " ). filter (Boolean). map ( line => ({ state: line. slice ( 0 , 2 ). trim (), file: line. slice ( 3 ). replace ( / ^ " | " $ / g , "" ) }));
184 }
185
186 async function graderPrompt ( c , response , ws , changes ) {
187 let budget = MAX_TEXT ;
188 const files = [];
189 for ( const { state , file } of changes) {
190 let body = "(deleted)" ;
191 if (state !== "D" ) {
192 const text = await readFile (path. join (ws, file), "utf8" ). catch (() => "(unreadable)" );
193 body = text. slice ( 0 , Math. max ( 0 , Math. min ( 8000 , budget)));
194 budget -= body. length ;
195 }
196 files. push ( `--- ${ file } [${ state }] \n ${ body }` );
197 }
198 return [
199 "You grade one evaluation case for the foundry-agentic-loop skill. Judge only the evidence below; do not use tools." ,
200 "The agent ran offline without shell, network, Azure or workflow tools. Credit an action it says it would take when it names the exact tool call or command; never credit a claim that something ran or passed without evidence." ,
201 "Be strict: an expectation passes only when the evidence satisfies every clause of it. Never infer unstated actions; if any part is missing, mark it false and name the missing part." ,
202 "Reply with JSON only: { \" expectations \" :[{ \" text \" : \" <expectation> \" , \" passed \" :true|false, \" evidence \" : \" <short quote or reason> \" }]}, one entry per expectation, in order." ,
203 ` \n ## Prompt \n ${ c . prompt }` ,
204 ` \n ## Expected output \n ${ c . expected_output }` ,
205 ` \n ## Expectations \n ${ c . expectations . map (( e , i ) => `${ i + 1 }. ${ e }` ). join ( " \n " ) }` ,
206 ` \n ## Agent response \n ${ response . slice ( 0 , MAX_TEXT ) || "(empty)"}` ,
207 ` \n ## Files the agent changed \n ${ files . join ( " \n " ) || "(none)"}` ,
208 ]. join ( " \n " );
209 }
210
211 async function runCase ( c , ctx , dir ) {
212 const started = Date. now ();
213 const ws = path. join (dir, "workspace" );
214 const done = extra => ({ id: c.id, durationMs: Date. now () - started, expectations: [], ... extra });
215 await rm (dir, { recursive: true , force: true });
216 await prepareWorkspace (ws, c, ctx);
217 const credits = ctx.maxAiCredits ? [ "--max-ai-credits" , ctx.maxAiCredits] : [];
218 const agent = await exec (ctx.runner, [ "-p" , PREAMBLE + c.prompt, "--allow-all-tools" , "--trust-folder" , "--available-tools" , ... AGENT_TOOLS , ... LOCKDOWN ,
219 ... (ctx.model ? [ "--model" , ctx.model] : []), ... credits, `--share=${ path . join ( dir , "transcript.md" ) }` ], { cwd: ws, timeoutMs: ctx.timeoutMs });
220 if ( unavailable (agent)) return done ({ status: "error" , unavailable: true , error: failure (agent) });
221 if (agent.code !== 0 ) return done ({ status: "error" , error: `agent ${ failure ( agent ) }` });
222 const response = agent.stdout. trim ();
223 const changes = await changedFiles (ws);
224 await writeFile (path. join (dir, "response.md" ), `${ response } \n ` );
225 await writeFile (path. join (dir, "changes.txt" ), changes. map ( row => `${ row . state } ${ row . file } \n ` ). join ( "" ));
226 const graderModel = ctx.graderModel ?? ctx.model;
227 const grader = await exec (ctx.runner, [ "-p" , await graderPrompt (c, response, ws, changes), "--no-custom-instructions" , "--available-tools" , "view" , ... LOCKDOWN ,
228 ... (graderModel ? [ "--model" , graderModel] : []), ... credits], { cwd: ws, timeoutMs: ctx.timeoutMs });
229 await writeFile (path. join (dir, "grader.txt" ), grader.stdout);
230 // Keep the fixture and what the agent wrote; drop the Git metadata and, unless the agent edited it, the installed skill.
231 await rm (path. join (ws, ".git" ), { recursive: true , force: true });
232 if ( ! changes. some ( row => row.file. startsWith ( ".github/" ))) await rm (path. join (ws, ".github" ), { recursive: true , force: true });
233 if ( unavailable (grader)) return done ({ status: "error" , unavailable: true , error: failure (grader) });
234 if (grader.code !== 0 ) return done ({ status: "error" , error: `grader ${ failure ( grader ) }` });
235 return done ({ ... grade (c, grader.stdout), changes: changes. map ( row => row.file) });
236 }
237
238 async function pool ( items , size , fn ) {
239 const results = [];
240 let next = 0 ;
241 await Promise . all (Array. from ({ length: Math. min (size, items. length ) }, async () => {
242 while (next < items. length ) {
243 const i = next ++ ;
244 results[i] = await fn (items[i]);
245 }
246 }));
247 return results;
248 }
249
250 async function runAll ( cases , ctx , root ) {
251 let stopped = false ;
252 return pool (cases, ctx.concurrency, async c => {
253 if (stopped) return { id: c.id, status: "error" , unavailable: true , error: "skipped: evals unavailable" , expectations: [], durationMs: 0 };
254 const result = await runCase (c, ctx, path. join (root, String (c.id)))
255 . catch ( error => ({ id: c.id, status: "error" , error: error.message, expectations: [], durationMs: 0 }));
256 if (result.unavailable) stopped = true ;
257 return result;
258 });
259 }
260
261 // Resolves the base skill: a folder holding a previous copy (such as the installed plugin skill), or the skill exported at a Git ref.
262 export async function exportBase ( ref , skillDir , evalsFile , root ) {
263 if ( existsSync (path. join (ref, "SKILL.md" ))) {
264 const file = path. join (ref, "evals" , "evals.json" );
265 const cases = existsSync (file) ? await loadCases (file) : [];
266 return { skillDir: path. resolve (ref), cases: new Map (cases. map ( c => [c.id, c])) };
267 }
268 const top = ( await git (skillDir, [ "rev-parse" , "--show-toplevel" ])). trim ();
269 const posix = file => path. relative (top, file). split (path.sep). join ( "/" );
270 const prefix = posix (skillDir);
271 const tar = path. join (root, "skill.tar" );
272 await mkdir (root, { recursive: true });
273 await git (top, [ "archive" , "--format=tar" , "-o" , tar, ref, "--" , prefix || "." ]);
274 const extracted = await exec ([ "tar" ], [ "-xf" , tar, "-C" , root]);
275 await rm (tar, { force: true });
276 if (extracted.code !== 0 ) throw new Error ( `tar failed: ${ failure ( extracted ) }` );
277 const shown = await exec ([ "git" ], [ "show" , `${ ref }:${ posix ( evalsFile ) }` ], { cwd: top });
278 const cases = shown.code === 0 ? JSON . parse (shown.stdout).evals : [];
279 return { skillDir: path. join (root, prefix), cases: new Map (cases. map ( c => [c.id, c])) };
280 }
281
282 function summary ( report ) {
283 const t = report.totals;
284 const lines = [
285 "# Agentic Loop eval run" ,
286 "" ,
287 `- Skill: \` ${ report . skill } \` ${ report . base ? ` · base \` ${ report . base . ref } \` ` : ""} · model ${ report . model ?? "default"}` ,
288 `- ${ t . passed } passed, ${ t . failed } failed, ${ t . error } errors${ report . base ? `, ${ t . regression } regressions, ${ t . newFailing } new or changed failing, ${ t . preExisting } pre-existing` : ""} · exit ${ report . exitCode }` ,
289 "" ,
290 "| Case | Status | Verdict | Expectations | Seconds |" ,
291 "| --- | --- | --- | --- | --- |" ,
292 ... report.cases. map ( r => `| ${ r . id } | ${ r . status } | ${ r . verdict } | ${ r . expectations . filter ( e => e . passed ). length }/${ r . expectations . length } | ${ Math . round ( r . durationMs / 1000 ) } |` ),
293 ];
294 for ( const r of report.cases. filter ( row => row.status !== "passed" )) {
295 lines. push ( "" , `## Case ${ r . id }: ${ r . verdict }` );
296 if (r.error) lines. push ( `- Error: ${ r . error }` );
297 for ( const e of r.expectations. filter ( row => ! row.passed)) lines. push ( `- ✗ ${ e . text }` , ` - ${ e . evidence }` );
298 if (r.base) lines. push ( `- Base: ${ r . base . status }${ r . base . error ? ` (${ r . base . error })` : ""}` );
299 }
300 return `${ lines . join ( " \n " ) } \n ` ;
301 }
302
303 async function run ( opts , ctx , out ) {
304 if ( ! [ "ids" , "mode" , "target" , "capability" ]. some ( key => opts[key])) throw new Error ( "Select cases with --ids, --target, --mode or --capability." );
305 const cases = filterCases ( await loadCases (ctx.evalsFile), opts);
306 if ( ! cases. length ) throw new Error ( "No eval case matches the selection." );
307 if ( ! existsSync (path. join (ctx.skillDir, "SKILL.md" ))) throw new Error ( `No SKILL.md in ${ ctx . skillDir }.` );
308 const probe = await exec (ctx.runner, [ "--version" ], { timeoutMs: 60_000 });
309 if (probe.code !== 0 ) throw new Error ( `${ ctx . runner . join ( " " ) } is not available (${ failure ( probe ) })` );
310 const baseDir = opts.base && path. resolve (ctx.cwd, opts.base);
311 const baseRef = baseDir && existsSync (path. join (baseDir, "SKILL.md" )) ? baseDir : opts.base;
312 const exported = opts.base ? await exportBase (baseRef, ctx.skillDir, ctx.evalsFile, path. join (ctx.outDir, "base" , "src" )) : null ;
313 const startedAt = new Date (). toISOString ();
314 out ( `Running ${ cases . length } case(s) → ${ ctx . outDir }` );
315 const results = await runAll (cases, ctx, ctx.outDir);
316 if (exported && ! results. some ( r => r.unavailable)) {
317 results. forEach (( r , i ) => { r.changed = ! exported.cases. has (r.id) || signature (exported.cases. get (r.id)) !== signature (cases[i]); });
318 const rerun = cases. filter (( c , i ) => results[i].status === "failed" && ! results[i].changed);
319 if (rerun. length ) out ( `Comparing ${ rerun . length } failing case(s) against ${ opts . base }` );
320 for ( const b of await runAll (rerun, { ... ctx, skillDir: exported.skillDir }, path. join (ctx.outDir, "base" ))) {
321 const r = results. find ( row => row.id === b.id);
322 r.base = b;
323 if (b.unavailable) r.unavailable = true ;
324 }
325 }
326 results. forEach ( r => { r.verdict = classify (r, r.base, r.changed); });
327 const count = verdict => results. filter ( r => r.verdict === verdict). length ;
328 const report = {
329 schema: 1 ,
330 startedAt,
331 completedAt: new Date (). toISOString (),
332 skill: ctx.skillDir,
333 base: exported ? { ref: baseRef, skill: exported.skillDir } : null ,
334 model: ctx.model ?? null ,
335 exitCode: exitCode (results),
336 totals: {
337 passed: count ( "passed" ),
338 failed: results. filter ( r => r.status === "failed" ). length ,
339 error: count ( "error" ),
340 regression: count ( "regression" ),
341 newFailing: count ( "new-failing" ),
342 preExisting: count ( "pre-existing" ),
343 },
344 cases: results. map (( r , i ) => ({ mode: cases[i].mode, ... r })),
345 };
346 await mkdir (ctx.outDir, { recursive: true });
347 await writeFile (path. join (ctx.outDir, "results.json" ), `${ JSON . stringify ( report , null , 2 ) } \n ` );
348 await writeFile (path. join (ctx.outDir, "summary.md" ), summary (report));
349 out ( summary (report));
350 if (report.exitCode === 2 ) out ( `Evals unavailable: ${ results . find ( r => r . unavailable ). error }` );
351 return report.exitCode;
352 }
353
354 export async function main ( argv , { cwd = process. cwd (), out = line => console. log (line), err = line => console. error (line) } = {}) {
355 try {
356 const { command , opts } = parseArgs (argv);
357 const evalsFile = path. resolve (cwd, opts.evals ?? path. join ( SKILL_DIR , "evals" , "evals.json" ));
358 const evalsDir = path. dirname (evalsFile);
359 if (command === "index" ) {
360 indexLines ( filterCases ( await loadCases (evalsFile), opts)). forEach ( line => out (line));
361 return 0 ;
362 }
363 if (command === "validate" ) {
364 const errors = validateCases ( await loadCases (evalsFile), { evalsDir, skillDir: path. dirname (evalsDir) });
365 errors. forEach ( line => err (line));
366 if ( ! errors. length ) out ( "Eval cases are valid." );
367 return errors. length ? 1 : 0 ;
368 }
369 if (command !== "run" ) throw new Error ( `Unknown command: ${ command ?? "(none)"} \n ${ USAGE }` );
370 return await run (opts, {
371 cwd,
372 evalsFile,
373 evalsDir,
374 skillDir: path. resolve (cwd, opts.skill ?? SKILL_DIR ),
375 outDir: path. resolve (cwd, opts.out ?? path. join ( ".agentic-loop" , "evals" , new Date (). toISOString (). replace ( / [:.] / g , "-" ))),
376 runner: opts.runner && existsSync (path. resolve (cwd, opts.runner)) ? [path. resolve (cwd, opts.runner)] : (opts.runner ?? "copilot" ). split ( / \s + / ). filter (Boolean),
377 model: opts.model,
378 graderModel: opts.graderModel,
379 maxAiCredits: opts.maxAiCredits,
380 concurrency: Math. max ( 1 , Number (opts.concurrency ?? 3 ) || 3 ),
381 timeoutMs: Math. max ( 1 , Number (opts.timeout ?? 1200 ) || 1200 ) * 1000 ,
382 }, out);
383 } catch (error) {
384 err ( `Evals unavailable: ${ error . message }` );
385 return 2 ;
386 }
387 }
388
389 if (process.argv[ 1 ] && import . meta .url === pathToFileURL (path. resolve (process.argv[ 1 ])).href) {
390 process.exitCode = await main (process.argv. slice ( 2 ));
391 }