atris 3.35.0 → 3.36.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/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { createBenchContext, withBenchContext } = require('./context');
|
|
7
|
+
const { ENGINE_NAMES, getEngineAdapter, normalizeEngineName } = require('./engines');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PACK = 'core-v1';
|
|
10
|
+
|
|
11
|
+
class BenchInfraError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'BenchInfraError';
|
|
15
|
+
this.exitCode = 2;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function repoRootFromHere() {
|
|
20
|
+
return path.resolve(__dirname, '..', '..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function benchmarksRoot(repoRoot = repoRootFromHere()) {
|
|
24
|
+
return path.join(repoRoot, 'atris', 'benchmarks');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function packDir(repoRoot = repoRootFromHere(), pack = DEFAULT_PACK) {
|
|
28
|
+
return path.join(benchmarksRoot(repoRoot), pack || DEFAULT_PACK);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultTasksDir(repoRoot = repoRootFromHere()) {
|
|
32
|
+
return packDir(repoRoot, DEFAULT_PACK);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateTaskSpec(spec, filePath) {
|
|
36
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
37
|
+
throw new BenchInfraError(`${filePath}: task spec must export an object`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof spec.id !== 'string' || !/^[a-z0-9-]+$/.test(spec.id)) {
|
|
40
|
+
throw new BenchInfraError(`${filePath}: task spec id must be kebab-case`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof spec.title !== 'string' || !spec.title.trim()) {
|
|
43
|
+
throw new BenchInfraError(`${spec.id}: title is required`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof spec.run !== 'function') {
|
|
46
|
+
throw new BenchInfraError(`${spec.id}: run(ctx) function is required`);
|
|
47
|
+
}
|
|
48
|
+
if (spec.timeoutMs !== undefined && (!Number.isFinite(Number(spec.timeoutMs)) || Number(spec.timeoutMs) <= 0)) {
|
|
49
|
+
throw new BenchInfraError(`${spec.id}: timeoutMs must be a positive number`);
|
|
50
|
+
}
|
|
51
|
+
if (spec.needsPython !== undefined && typeof spec.needsPython !== 'boolean') {
|
|
52
|
+
throw new BenchInfraError(`${spec.id}: needsPython must be boolean when present`);
|
|
53
|
+
}
|
|
54
|
+
return spec;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateAgentTaskSpec(spec, filePath) {
|
|
58
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
59
|
+
throw new BenchInfraError(`${filePath}: task spec must export an object`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof spec.id !== 'string' || !/^[a-z0-9-]+$/.test(spec.id)) {
|
|
62
|
+
throw new BenchInfraError(`${filePath}: task spec id must be kebab-case`);
|
|
63
|
+
}
|
|
64
|
+
if (typeof spec.title !== 'string' || !spec.title.trim()) {
|
|
65
|
+
throw new BenchInfraError(`${spec.id}: title is required`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof spec.category !== 'string' || !spec.category.trim()) {
|
|
68
|
+
throw new BenchInfraError(`${spec.id}: category is required`);
|
|
69
|
+
}
|
|
70
|
+
if (typeof spec.check !== 'function') {
|
|
71
|
+
throw new BenchInfraError(`${spec.id}: check(ctx) function is required`);
|
|
72
|
+
}
|
|
73
|
+
if (spec.timeoutMs !== undefined && (!Number.isFinite(Number(spec.timeoutMs)) || Number(spec.timeoutMs) <= 0)) {
|
|
74
|
+
throw new BenchInfraError(`${spec.id}: timeoutMs must be a positive number`);
|
|
75
|
+
}
|
|
76
|
+
return spec;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function discoverPacks(options = {}) {
|
|
80
|
+
const repoRoot = path.resolve(options.repoRoot || repoRootFromHere());
|
|
81
|
+
const root = benchmarksRoot(repoRoot);
|
|
82
|
+
if (!fs.existsSync(root)) return [];
|
|
83
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
84
|
+
.filter((entry) => entry.isDirectory())
|
|
85
|
+
.map((entry) => entry.name)
|
|
86
|
+
.sort((a, b) => a.localeCompare(b));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadTaskSpecs(options = {}) {
|
|
90
|
+
const repoRoot = path.resolve(options.repoRoot || repoRootFromHere());
|
|
91
|
+
const tasksDir = path.resolve(options.tasksDir || packDir(repoRoot, options.pack || DEFAULT_PACK));
|
|
92
|
+
if (!fs.existsSync(tasksDir)) throw new BenchInfraError(`benchmark task directory not found: ${tasksDir}`);
|
|
93
|
+
const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
|
|
94
|
+
const files = entries
|
|
95
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.js'))
|
|
96
|
+
.map((entry) => entry.name)
|
|
97
|
+
.sort((a, b) => a.localeCompare(b));
|
|
98
|
+
const taskDirs = entries
|
|
99
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(tasksDir, entry.name, 'check.js')))
|
|
100
|
+
.map((entry) => entry.name)
|
|
101
|
+
.sort((a, b) => a.localeCompare(b));
|
|
102
|
+
if (!files.length && !taskDirs.length) throw new BenchInfraError(`no benchmark task specs found in ${tasksDir}`);
|
|
103
|
+
|
|
104
|
+
const specs = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const filePath = path.join(tasksDir, file);
|
|
108
|
+
delete require.cache[require.resolve(filePath)];
|
|
109
|
+
const spec = validateTaskSpec(require(filePath), filePath);
|
|
110
|
+
if (seen.has(spec.id)) throw new BenchInfraError(`duplicate benchmark task id: ${spec.id}`);
|
|
111
|
+
seen.add(spec.id);
|
|
112
|
+
specs.push({
|
|
113
|
+
...spec,
|
|
114
|
+
kind: 'legacy',
|
|
115
|
+
taskPath: filePath,
|
|
116
|
+
orderKey: file,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const dirName of taskDirs) {
|
|
120
|
+
const taskDir = path.join(tasksDir, dirName);
|
|
121
|
+
const checkPath = path.join(taskDir, 'check.js');
|
|
122
|
+
const promptPath = path.join(taskDir, 'prompt.md');
|
|
123
|
+
const fixtureDir = path.join(taskDir, 'fixture');
|
|
124
|
+
const solutionPath = path.join(taskDir, 'solution.sh');
|
|
125
|
+
for (const requiredPath of [promptPath, fixtureDir, solutionPath]) {
|
|
126
|
+
if (!fs.existsSync(requiredPath)) throw new BenchInfraError(`${taskDir}: missing ${path.basename(requiredPath)}`);
|
|
127
|
+
}
|
|
128
|
+
delete require.cache[require.resolve(checkPath)];
|
|
129
|
+
const spec = validateAgentTaskSpec(require(checkPath), checkPath);
|
|
130
|
+
if (seen.has(spec.id)) throw new BenchInfraError(`duplicate benchmark task id: ${spec.id}`);
|
|
131
|
+
seen.add(spec.id);
|
|
132
|
+
specs.push({
|
|
133
|
+
id: spec.id,
|
|
134
|
+
title: spec.title,
|
|
135
|
+
category: spec.category,
|
|
136
|
+
timeoutMs: spec.timeoutMs,
|
|
137
|
+
kind: 'agent',
|
|
138
|
+
check: spec.check,
|
|
139
|
+
taskDir,
|
|
140
|
+
fixtureDir,
|
|
141
|
+
promptPath,
|
|
142
|
+
solutionPath,
|
|
143
|
+
setupPath: fs.existsSync(path.join(taskDir, 'setup.js')) ? path.join(taskDir, 'setup.js') : null,
|
|
144
|
+
orderKey: dirName,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return specs.sort((a, b) => a.orderKey.localeCompare(b.orderKey));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findPython() {
|
|
151
|
+
for (const candidate of ['python3']) {
|
|
152
|
+
const result = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
|
|
153
|
+
if (!result.error && result.status === 0) return candidate;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isInfraFailure(err) {
|
|
159
|
+
if (!err) return false;
|
|
160
|
+
if (err.spawnError) return true;
|
|
161
|
+
if (['ETIMEDOUT', 'EAGAIN', 'ENOMEM', 'ENOBUFS'].includes(err.code)) return true;
|
|
162
|
+
if (typeof err.syscall === 'string' && err.syscall.startsWith('spawn')) return true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function timeoutError(spec, timeoutMs) {
|
|
167
|
+
const err = new Error(`${spec.id} timed out after ${timeoutMs}ms`);
|
|
168
|
+
err.code = 'ETIMEDOUT';
|
|
169
|
+
return err;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function withTimeout(promise, spec, timeoutMs) {
|
|
173
|
+
let timer = null;
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
timer = setTimeout(() => reject(timeoutError(spec, timeoutMs)), timeoutMs);
|
|
176
|
+
Promise.resolve(promise).then(
|
|
177
|
+
(value) => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
resolve(value);
|
|
180
|
+
},
|
|
181
|
+
(err) => {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
reject(err);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatFailure(err) {
|
|
190
|
+
if (!err) return 'unknown failure';
|
|
191
|
+
return String(err.stack || err.message || err);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runTaskAttempt(spec, options) {
|
|
195
|
+
const timeoutMs = Number(spec.timeoutMs || options.timeoutMs || 30000);
|
|
196
|
+
return withBenchContext({
|
|
197
|
+
repoRoot: options.repoRoot,
|
|
198
|
+
taskId: spec.id,
|
|
199
|
+
timeoutMs,
|
|
200
|
+
}, async (ctx) => withTimeout(spec.run(ctx), spec, timeoutMs));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function copyFixtureIntoWorkspace(fixtureDir, workspace) {
|
|
204
|
+
fs.cpSync(fixtureDir, workspace, {
|
|
205
|
+
recursive: true,
|
|
206
|
+
force: true,
|
|
207
|
+
dereference: false,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runSetup(spec, ctx) {
|
|
212
|
+
if (!spec.setupPath) return;
|
|
213
|
+
delete require.cache[require.resolve(spec.setupPath)];
|
|
214
|
+
const setupModule = require(spec.setupPath);
|
|
215
|
+
const setup = typeof setupModule === 'function' ? setupModule : setupModule && setupModule.setup;
|
|
216
|
+
if (typeof setup !== 'function') {
|
|
217
|
+
throw new BenchInfraError(`${spec.id}: setup.js must export a function or { setup }`);
|
|
218
|
+
}
|
|
219
|
+
await setup(ctx);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatEngineFailure(engineName, result) {
|
|
223
|
+
const stdout = String(result && result.stdout ? result.stdout : '').trim();
|
|
224
|
+
const stderr = String(result && result.stderr ? result.stderr : '').trim();
|
|
225
|
+
const details = [stderr, stdout].filter(Boolean).join('\n').slice(0, 4000);
|
|
226
|
+
return `engine ${engineName} exited ${result ? result.status : 'unknown'}${details ? `\n${details}` : ''}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function runAgentTaskSpec(spec, options = {}) {
|
|
230
|
+
const started = Date.now();
|
|
231
|
+
const timeoutMs = Number(spec.timeoutMs || options.timeoutMs || 300000);
|
|
232
|
+
let bench = null;
|
|
233
|
+
try {
|
|
234
|
+
bench = createBenchContext({
|
|
235
|
+
repoRoot: options.repoRoot,
|
|
236
|
+
taskId: spec.id,
|
|
237
|
+
timeoutMs,
|
|
238
|
+
});
|
|
239
|
+
const ctx = bench.ctx;
|
|
240
|
+
ctx.taskDir = spec.taskDir;
|
|
241
|
+
ctx.fixtureDir = spec.fixtureDir;
|
|
242
|
+
ctx.promptPath = spec.promptPath;
|
|
243
|
+
copyFixtureIntoWorkspace(spec.fixtureDir, ctx.workspace);
|
|
244
|
+
|
|
245
|
+
const engine = getEngineAdapter(options.engine, { solutionPath: spec.solutionPath });
|
|
246
|
+
const availability = engine.available(ctx.workspace);
|
|
247
|
+
if (!availability.available) {
|
|
248
|
+
return {
|
|
249
|
+
id: spec.id,
|
|
250
|
+
passed: false,
|
|
251
|
+
skipped: true,
|
|
252
|
+
failures: [],
|
|
253
|
+
duration_ms: Date.now() - started,
|
|
254
|
+
retried: false,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await runSetup(spec, ctx);
|
|
259
|
+
const promptText = fs.readFileSync(spec.promptPath, 'utf8');
|
|
260
|
+
const engineResult = await withTimeout(Promise.resolve(engine.run(promptText, ctx.workspace, timeoutMs)), spec, timeoutMs);
|
|
261
|
+
if (!engineResult || engineResult.status !== 0) {
|
|
262
|
+
throw new Error(formatEngineFailure(options.engine, engineResult));
|
|
263
|
+
}
|
|
264
|
+
await withTimeout(Promise.resolve().then(() => spec.check(ctx)), spec, timeoutMs);
|
|
265
|
+
return {
|
|
266
|
+
id: spec.id,
|
|
267
|
+
passed: true,
|
|
268
|
+
skipped: false,
|
|
269
|
+
failures: [],
|
|
270
|
+
duration_ms: Date.now() - started,
|
|
271
|
+
retried: false,
|
|
272
|
+
};
|
|
273
|
+
} catch (err) {
|
|
274
|
+
return {
|
|
275
|
+
id: spec.id,
|
|
276
|
+
passed: false,
|
|
277
|
+
skipped: false,
|
|
278
|
+
failures: [formatFailure(err)],
|
|
279
|
+
duration_ms: Date.now() - started,
|
|
280
|
+
retried: false,
|
|
281
|
+
};
|
|
282
|
+
} finally {
|
|
283
|
+
if (bench) bench.teardown();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function runTaskSpec(spec, options = {}) {
|
|
288
|
+
if (spec.kind === 'agent') return runAgentTaskSpec(spec, options);
|
|
289
|
+
|
|
290
|
+
const started = Date.now();
|
|
291
|
+
let retried = false;
|
|
292
|
+
|
|
293
|
+
if (spec.needsPython && !options.pythonCmd) {
|
|
294
|
+
return {
|
|
295
|
+
id: spec.id,
|
|
296
|
+
passed: false,
|
|
297
|
+
skipped: true,
|
|
298
|
+
failures: [],
|
|
299
|
+
duration_ms: Date.now() - started,
|
|
300
|
+
retried: false,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
305
|
+
try {
|
|
306
|
+
const value = await runTaskAttempt(spec, options);
|
|
307
|
+
if (value && value.skipped) {
|
|
308
|
+
return {
|
|
309
|
+
id: spec.id,
|
|
310
|
+
passed: false,
|
|
311
|
+
skipped: true,
|
|
312
|
+
failures: [],
|
|
313
|
+
duration_ms: Date.now() - started,
|
|
314
|
+
retried,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
id: spec.id,
|
|
319
|
+
passed: true,
|
|
320
|
+
skipped: false,
|
|
321
|
+
failures: [],
|
|
322
|
+
duration_ms: Date.now() - started,
|
|
323
|
+
retried,
|
|
324
|
+
};
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const infra = isInfraFailure(err);
|
|
327
|
+
if (infra && attempt === 0) {
|
|
328
|
+
retried = true;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
id: spec.id,
|
|
333
|
+
passed: false,
|
|
334
|
+
skipped: false,
|
|
335
|
+
failures: [formatFailure(err)],
|
|
336
|
+
duration_ms: Date.now() - started,
|
|
337
|
+
retried,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
id: spec.id,
|
|
344
|
+
passed: false,
|
|
345
|
+
skipped: false,
|
|
346
|
+
failures: ['unreachable benchmark runner state'],
|
|
347
|
+
duration_ms: Date.now() - started,
|
|
348
|
+
retried,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function selectTaskSpecs(specs, taskIds = []) {
|
|
353
|
+
const ids = (taskIds || []).filter(Boolean);
|
|
354
|
+
if (!ids.length) return specs;
|
|
355
|
+
const byId = new Map(specs.map((spec) => [spec.id, spec]));
|
|
356
|
+
return ids.map((id) => {
|
|
357
|
+
const spec = byId.get(id);
|
|
358
|
+
if (!spec) throw new BenchInfraError(`unknown benchmark task: ${id}`);
|
|
359
|
+
return spec;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runTaskSpecs(specs, options = {}) {
|
|
364
|
+
const records = [];
|
|
365
|
+
for (const spec of specs) {
|
|
366
|
+
records.push(await runTaskSpec(spec, options));
|
|
367
|
+
}
|
|
368
|
+
return records;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizeBenchEngine(engine) {
|
|
372
|
+
if (engine === undefined || engine === null || engine === '') return null;
|
|
373
|
+
try {
|
|
374
|
+
return normalizeEngineName(engine);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
throw new BenchInfraError(err.message);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function normalizeLabel(label) {
|
|
381
|
+
if (label === undefined || label === null || label === '') return null;
|
|
382
|
+
if (label === 'baseline' || label === 'candidate') return label;
|
|
383
|
+
throw new BenchInfraError(`invalid bench label: ${label}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function benchStateDir(stateRoot = process.cwd()) {
|
|
387
|
+
return path.join(stateRoot, '.atris', 'state', 'bench');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function appendResultRecord(record, stateRoot = process.cwd()) {
|
|
391
|
+
const dir = benchStateDir(stateRoot);
|
|
392
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
393
|
+
const outPath = path.join(dir, 'results.jsonl');
|
|
394
|
+
fs.appendFileSync(outPath, `${JSON.stringify(record)}\n`, 'utf8');
|
|
395
|
+
return outPath;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function writeBaselineRecord(record, stateRoot = process.cwd()) {
|
|
399
|
+
const dir = benchStateDir(stateRoot);
|
|
400
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
401
|
+
const outPath = path.join(dir, 'baseline.json');
|
|
402
|
+
fs.writeFileSync(outPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
403
|
+
return outPath;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function readResultRecords(options = {}) {
|
|
407
|
+
const filePath = path.join(benchStateDir(options.stateRoot || process.cwd()), 'results.jsonl');
|
|
408
|
+
if (!fs.existsSync(filePath)) return [];
|
|
409
|
+
const rows = fs.readFileSync(filePath, 'utf8')
|
|
410
|
+
.split(/\r?\n/)
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.map((line) => JSON.parse(line));
|
|
413
|
+
const last = Number(options.last || 0);
|
|
414
|
+
return last > 0 ? rows.slice(-last) : rows;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function summarizeTasks(tasks) {
|
|
418
|
+
const passed = tasks.filter((task) => task.passed && !task.skipped).map((task) => task.id);
|
|
419
|
+
const failed = tasks.filter((task) => !task.passed && !task.skipped).map((task) => task.id);
|
|
420
|
+
const skipped = tasks.filter((task) => task.skipped).map((task) => task.id);
|
|
421
|
+
const denominator = Math.max(0, tasks.length - skipped.length);
|
|
422
|
+
return {
|
|
423
|
+
passed,
|
|
424
|
+
failed,
|
|
425
|
+
skipped,
|
|
426
|
+
summary: `${passed.length}/${denominator} gate cases passed`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function exitCodeForRecord(record) {
|
|
431
|
+
return record.failed.length > 0 ? 1 : 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function runBench(options = {}) {
|
|
435
|
+
const repoRoot = path.resolve(options.repoRoot || repoRootFromHere());
|
|
436
|
+
const pack = options.pack || (options.tasksDir ? path.basename(path.resolve(options.tasksDir)) : DEFAULT_PACK);
|
|
437
|
+
const specs = selectTaskSpecs(loadTaskSpecs({ repoRoot, tasksDir: options.tasksDir, pack }), options.taskIds);
|
|
438
|
+
const engine = normalizeBenchEngine(options.engine);
|
|
439
|
+
if (specs.some((spec) => spec.kind === 'agent') && !engine) {
|
|
440
|
+
throw new BenchInfraError(`agent benchmark pack ${pack} requires --engine <${ENGINE_NAMES.join('|')}>`);
|
|
441
|
+
}
|
|
442
|
+
const label = normalizeLabel(options.label);
|
|
443
|
+
const started = new Date().toISOString();
|
|
444
|
+
const pythonCmd = options.pythonCmd === undefined ? findPython() : options.pythonCmd;
|
|
445
|
+
const taskRecords = await runTaskSpecs(specs, {
|
|
446
|
+
repoRoot,
|
|
447
|
+
timeoutMs: options.timeoutMs,
|
|
448
|
+
pythonCmd,
|
|
449
|
+
engine,
|
|
450
|
+
});
|
|
451
|
+
const finished = new Date().toISOString();
|
|
452
|
+
const summary = summarizeTasks(taskRecords);
|
|
453
|
+
const record = {
|
|
454
|
+
schema: 'atris.bench.run.v1',
|
|
455
|
+
pack,
|
|
456
|
+
engine,
|
|
457
|
+
label,
|
|
458
|
+
experiment: options.experiment || null,
|
|
459
|
+
started,
|
|
460
|
+
finished,
|
|
461
|
+
tasks: taskRecords,
|
|
462
|
+
passed: summary.passed,
|
|
463
|
+
failed: summary.failed,
|
|
464
|
+
skipped: summary.skipped,
|
|
465
|
+
summary: summary.summary,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
if (options.persist !== false) appendResultRecord(record, options.stateRoot || process.cwd());
|
|
469
|
+
if (options.updateBaseline) writeBaselineRecord(record, options.stateRoot || process.cwd());
|
|
470
|
+
return { record, exitCode: exitCodeForRecord(record) };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function taskMetadata(options = {}) {
|
|
474
|
+
return loadTaskSpecs(options).map((spec) => ({
|
|
475
|
+
id: spec.id,
|
|
476
|
+
title: spec.title,
|
|
477
|
+
category: spec.category || null,
|
|
478
|
+
timeoutMs: spec.timeoutMs || null,
|
|
479
|
+
needsPython: spec.needsPython === true,
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function packMetadata(options = {}) {
|
|
484
|
+
const repoRoot = path.resolve(options.repoRoot || repoRootFromHere());
|
|
485
|
+
return discoverPacks({ repoRoot }).map((id) => ({
|
|
486
|
+
id,
|
|
487
|
+
default: id === DEFAULT_PACK,
|
|
488
|
+
taskCount: loadTaskSpecs({ repoRoot, pack: id }).length,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
module.exports = {
|
|
493
|
+
BenchInfraError,
|
|
494
|
+
DEFAULT_PACK,
|
|
495
|
+
appendResultRecord,
|
|
496
|
+
benchmarksRoot,
|
|
497
|
+
defaultTasksDir,
|
|
498
|
+
discoverPacks,
|
|
499
|
+
exitCodeForRecord,
|
|
500
|
+
findPython,
|
|
501
|
+
isInfraFailure,
|
|
502
|
+
loadTaskSpecs,
|
|
503
|
+
packMetadata,
|
|
504
|
+
readResultRecords,
|
|
505
|
+
runBench,
|
|
506
|
+
runTaskSpec,
|
|
507
|
+
runTaskSpecs,
|
|
508
|
+
selectTaskSpecs,
|
|
509
|
+
summarizeTasks,
|
|
510
|
+
taskMetadata,
|
|
511
|
+
writeBaselineRecord,
|
|
512
|
+
};
|