atris 3.46.1 → 3.48.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/atris/skills/design/SKILL.md +2 -1
- package/atris/skills/youtube/SKILL.md +5 -3
- package/bin/atris.js +3 -1
- package/commands/ci.js +1 -1
- package/commands/close.js +167 -6
- package/commands/mission.js +1 -22
- package/commands/youtube.js +31 -1
- package/lib/ci-runner.js +215 -28
- package/lib/engine-ask.js +15 -1
- package/lib/engine-registry.js +22 -0
- package/lib/fleet.js +15 -0
- package/package.json +2 -1
- package/scripts/det/README.md +162 -0
- package/scripts/det/ax-lane-eval.js +116 -0
- package/scripts/det/changelog.js +148 -0
- package/scripts/det/codex-watchdog.js +217 -0
- package/scripts/det/commit-msg.js +153 -0
- package/scripts/det/data/ax-lane-gold.jsonl +81 -0
- package/scripts/det/data/ax-lane-holdout.jsonl +20 -0
- package/scripts/det/date.js +91 -0
- package/scripts/det/det.js +149 -0
- package/scripts/det/extract.js +93 -0
- package/scripts/det/hash.js +79 -0
- package/scripts/det/hunk-filter.js +73 -0
- package/scripts/det/json.js +120 -0
- package/scripts/det/pr-description.js +213 -0
- package/scripts/det/test.js +296 -0
- package/scripts/det/text.js +102 -0
- package/scripts/det/voice.js +76 -0
- package/scripts/det/ytnotes +196 -0
- package/scripts/det/ytquote-repair.js +181 -0
- package/scripts/det/ytrail-eval.js +124 -0
package/lib/ci-runner.js
CHANGED
|
@@ -25,6 +25,14 @@ function clockDate(clock) {
|
|
|
25
25
|
return date;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function parseRunnerMarker(line) {
|
|
29
|
+
const text = String(line);
|
|
30
|
+
if (text.includes('Running job:')) return { type: 'start' };
|
|
31
|
+
const completed = text.match(/completed with result:\s*([a-z]+)/i);
|
|
32
|
+
if (completed) return { type: 'complete', result: completed[1].toLowerCase() };
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
function runnerAssetName(platform, arch, version = RUNNER_VERSION) {
|
|
29
37
|
const platformNames = { darwin: 'osx', linux: 'linux' };
|
|
30
38
|
const archNames = { x64: 'x64', arm64: 'arm64' };
|
|
@@ -53,14 +61,19 @@ function parseRepo(value) {
|
|
|
53
61
|
|
|
54
62
|
function parseRunnerArgs(argv) {
|
|
55
63
|
const options = { label: null, once: false };
|
|
56
|
-
|
|
64
|
+
const repos = [];
|
|
65
|
+
const seenRepos = new Set();
|
|
57
66
|
|
|
58
67
|
for (let index = 0; index < argv.length; index += 1) {
|
|
59
68
|
const arg = argv[index];
|
|
60
69
|
if (arg === '--repo') {
|
|
61
|
-
|
|
62
|
-
repoValue = argv[index + 1];
|
|
70
|
+
const repoValue = argv[index + 1];
|
|
63
71
|
if (!repoValue || repoValue.startsWith('--')) throw new Error('--repo owner/name is required');
|
|
72
|
+
const repo = parseRepo(repoValue);
|
|
73
|
+
const key = repo.slug.toLowerCase();
|
|
74
|
+
if (seenRepos.has(key)) throw new Error(`duplicate --repo: ${repo.slug}`);
|
|
75
|
+
seenRepos.add(key);
|
|
76
|
+
repos.push(repo);
|
|
64
77
|
index += 1;
|
|
65
78
|
} else if (arg === '--label') {
|
|
66
79
|
if (options.label !== null) throw new Error('--label may only be set once');
|
|
@@ -78,7 +91,8 @@ function parseRunnerArgs(argv) {
|
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
|
|
94
|
+
if (repos.length === 0) throw new Error('--repo owner/name is required');
|
|
95
|
+
return { repos, label: options.label, once: options.once };
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
function parseUsageArgs(argv) {
|
|
@@ -143,6 +157,11 @@ function summarizeUsage(records, options = {}) {
|
|
|
143
157
|
const repo = repos.get(record.repo) || { repo: record.repo, jobs: 0, minutes: 0 };
|
|
144
158
|
repo.jobs += 1;
|
|
145
159
|
repo.minutes += minutes;
|
|
160
|
+
if (typeof record.result === 'string' && /^[a-z]+$/i.test(record.result)) {
|
|
161
|
+
if (!repo.results) repo.results = {};
|
|
162
|
+
const result = record.result.toLowerCase();
|
|
163
|
+
repo.results[result] = (repo.results[result] || 0) + 1;
|
|
164
|
+
}
|
|
146
165
|
repos.set(record.repo, repo);
|
|
147
166
|
}
|
|
148
167
|
|
|
@@ -161,10 +180,25 @@ function formatUsageSummary(summary) {
|
|
|
161
180
|
`total minutes: ${summary.totalMinutes}`,
|
|
162
181
|
`minutes this month: ${summary.monthMinutes}`,
|
|
163
182
|
'per repo:',
|
|
164
|
-
...summary.repos.map((repo) =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
...summary.repos.map((repo) => {
|
|
184
|
+
const resultOrder = ['succeeded', 'failed', 'cancelled', 'skipped'];
|
|
185
|
+
const results = repo.results
|
|
186
|
+
? Object.entries(repo.results).sort(([left], [right]) => {
|
|
187
|
+
const leftIndex = resultOrder.indexOf(left);
|
|
188
|
+
const rightIndex = resultOrder.indexOf(right);
|
|
189
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
190
|
+
return (leftIndex === -1 ? resultOrder.length : leftIndex)
|
|
191
|
+
- (rightIndex === -1 ? resultOrder.length : rightIndex);
|
|
192
|
+
}
|
|
193
|
+
return left.localeCompare(right);
|
|
194
|
+
})
|
|
195
|
+
: [];
|
|
196
|
+
return [
|
|
197
|
+
`${repo.repo}: ${repo.jobs} ${repo.jobs === 1 ? 'job' : 'jobs'}`,
|
|
198
|
+
...results.map(([result, count]) => `${count} ${result}`),
|
|
199
|
+
`${repo.minutes} ${repo.minutes === 1 ? 'minute' : 'minutes'}`,
|
|
200
|
+
].join(', ');
|
|
201
|
+
}),
|
|
168
202
|
].join('\n');
|
|
169
203
|
}
|
|
170
204
|
|
|
@@ -314,21 +348,130 @@ async function ensureRunner(options = {}) {
|
|
|
314
348
|
return runnerDir;
|
|
315
349
|
}
|
|
316
350
|
|
|
317
|
-
function
|
|
351
|
+
function linkRunnerTree(source, destination) {
|
|
352
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
353
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
354
|
+
const sourcePath = path.join(source, entry.name);
|
|
355
|
+
const destinationPath = path.join(destination, entry.name);
|
|
356
|
+
if (fs.existsSync(destinationPath)) continue;
|
|
357
|
+
if (entry.isDirectory()) {
|
|
358
|
+
linkRunnerTree(sourcePath, destinationPath);
|
|
359
|
+
} else if (entry.isSymbolicLink()) {
|
|
360
|
+
fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath);
|
|
361
|
+
} else {
|
|
362
|
+
try {
|
|
363
|
+
fs.linkSync(sourcePath, destinationPath);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (!error || error.code !== 'EXDEV') throw error;
|
|
366
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function ensureRepoRunnerDirectory(runnerDir, repo) {
|
|
373
|
+
const destination = path.join(runnerDir, 'repos', repo.owner.toLowerCase(), repo.repo.toLowerCase());
|
|
374
|
+
if (fs.existsSync(path.join(destination, 'run.sh'))) return destination;
|
|
375
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
376
|
+
const ignored = new Set([
|
|
377
|
+
'.credentials',
|
|
378
|
+
'.credentials_rsaparams',
|
|
379
|
+
'.runner',
|
|
380
|
+
'.service',
|
|
381
|
+
'_diag',
|
|
382
|
+
'_work',
|
|
383
|
+
'repos',
|
|
384
|
+
]);
|
|
385
|
+
for (const entry of fs.readdirSync(runnerDir, { withFileTypes: true })) {
|
|
386
|
+
if (ignored.has(entry.name)) continue;
|
|
387
|
+
const sourcePath = path.join(runnerDir, entry.name);
|
|
388
|
+
const destinationPath = path.join(destination, entry.name);
|
|
389
|
+
if (fs.existsSync(destinationPath)) continue;
|
|
390
|
+
if (entry.isDirectory()) {
|
|
391
|
+
linkRunnerTree(sourcePath, destinationPath);
|
|
392
|
+
} else if (entry.isSymbolicLink()) {
|
|
393
|
+
fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath);
|
|
394
|
+
} else {
|
|
395
|
+
try {
|
|
396
|
+
fs.linkSync(sourcePath, destinationPath);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (!error || error.code !== 'EXDEV') throw error;
|
|
399
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!fs.existsSync(path.join(destination, 'run.sh'))) {
|
|
404
|
+
throw new Error('github actions runner cache did not contain run.sh');
|
|
405
|
+
}
|
|
406
|
+
return destination;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function forwardRunnerStream(stream, output, prefix, inspectLine) {
|
|
410
|
+
if (!stream) return;
|
|
411
|
+
let pending = '';
|
|
412
|
+
stream.setEncoding('utf8');
|
|
413
|
+
stream.on('data', (chunk) => {
|
|
414
|
+
pending += chunk;
|
|
415
|
+
let newline = pending.indexOf('\n');
|
|
416
|
+
while (newline !== -1) {
|
|
417
|
+
const line = pending.slice(0, newline).replace(/\r$/, '');
|
|
418
|
+
pending = pending.slice(newline + 1);
|
|
419
|
+
inspectLine(line);
|
|
420
|
+
output.write(`${prefix}${line}\n`);
|
|
421
|
+
newline = pending.indexOf('\n');
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
stream.on('end', () => {
|
|
425
|
+
if (!pending) return;
|
|
426
|
+
const line = pending.replace(/\r$/, '');
|
|
427
|
+
inspectLine(line);
|
|
428
|
+
output.write(`${prefix}${line}`);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function runWorker(runnerDir, jitConfig, options = {}) {
|
|
318
433
|
return new Promise((resolve, reject) => {
|
|
319
|
-
const
|
|
434
|
+
const clock = options.clock || (() => new Date());
|
|
435
|
+
const child = (options.start || spawn)('./run.sh', ['--jitconfig', jitConfig], {
|
|
320
436
|
cwd: runnerDir,
|
|
321
437
|
env: process.env,
|
|
322
|
-
stdio: 'inherit',
|
|
438
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
323
439
|
});
|
|
324
|
-
|
|
325
|
-
|
|
440
|
+
let startedAt = null;
|
|
441
|
+
let finishedAt = null;
|
|
442
|
+
let result = null;
|
|
443
|
+
let settled = false;
|
|
444
|
+
const measurement = () => ({ startedAt, finishedAt, result });
|
|
445
|
+
const inspectLine = (line) => {
|
|
446
|
+
const marker = parseRunnerMarker(line);
|
|
447
|
+
if (!marker) return;
|
|
448
|
+
if (marker.type === 'start' && !startedAt) startedAt = clockDate(clock);
|
|
449
|
+
if (marker.type === 'complete' && !finishedAt) {
|
|
450
|
+
finishedAt = clockDate(clock);
|
|
451
|
+
result = marker.result;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
forwardRunnerStream(child.stdout, options.stdout || process.stdout, options.prefix || '', inspectLine);
|
|
455
|
+
forwardRunnerStream(child.stderr, options.stderr || process.stderr, options.prefix || '', inspectLine);
|
|
456
|
+
child.on('error', (error) => {
|
|
457
|
+
if (settled) return;
|
|
458
|
+
settled = true;
|
|
459
|
+
error.ciUsage = measurement();
|
|
460
|
+
reject(error);
|
|
461
|
+
});
|
|
462
|
+
child.on('close', (code, signal) => {
|
|
463
|
+
if (settled) return;
|
|
464
|
+
settled = true;
|
|
326
465
|
if (code === 0) {
|
|
327
|
-
resolve();
|
|
466
|
+
resolve(measurement());
|
|
328
467
|
} else if (signal) {
|
|
329
|
-
|
|
468
|
+
const error = new Error(`github actions runner stopped with signal ${signal}`);
|
|
469
|
+
error.ciUsage = measurement();
|
|
470
|
+
reject(error);
|
|
330
471
|
} else {
|
|
331
|
-
|
|
472
|
+
const error = new Error(`github actions runner exited with code ${code}`);
|
|
473
|
+
error.ciUsage = measurement();
|
|
474
|
+
reject(error);
|
|
332
475
|
}
|
|
333
476
|
});
|
|
334
477
|
});
|
|
@@ -348,20 +491,39 @@ async function runJobLoop(options, dependencies = {}) {
|
|
|
348
491
|
const runnerName = `${options.runnerName}-${completedJobs + 1}`;
|
|
349
492
|
const jitConfig = await mint(options.token, options.repo, options.label, runnerName);
|
|
350
493
|
log(`worker ready, waiting for jobs on ${options.repo.slug}`);
|
|
351
|
-
const startedAt = clockDate(clock);
|
|
352
494
|
let workerError = null;
|
|
495
|
+
let workerUsage = null;
|
|
353
496
|
try {
|
|
354
|
-
await startWorker(options.runnerDir, jitConfig
|
|
497
|
+
workerUsage = await startWorker(options.runnerDir, jitConfig, {
|
|
498
|
+
clock,
|
|
499
|
+
prefix: options.logPrefix || '',
|
|
500
|
+
start: dependencies.spawn,
|
|
501
|
+
stderr: dependencies.stderr,
|
|
502
|
+
stdout: dependencies.stdout,
|
|
503
|
+
});
|
|
355
504
|
} catch (error) {
|
|
356
505
|
workerError = error;
|
|
506
|
+
workerUsage = error && error.ciUsage;
|
|
357
507
|
} finally {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
508
|
+
const markerStart = workerUsage && workerUsage.startedAt
|
|
509
|
+
? clockDate(() => workerUsage.startedAt)
|
|
510
|
+
: null;
|
|
511
|
+
const markerFinish = workerUsage && workerUsage.finishedAt
|
|
512
|
+
? clockDate(() => workerUsage.finishedAt)
|
|
513
|
+
: null;
|
|
514
|
+
const startedAt = markerStart || markerFinish || clockDate(clock);
|
|
515
|
+
const durationSeconds = markerStart && markerFinish
|
|
516
|
+
? Math.max(0, Math.ceil((markerFinish.getTime() - markerStart.getTime()) / 1000))
|
|
517
|
+
: 0;
|
|
518
|
+
const record = {
|
|
361
519
|
repo: options.repo.slug,
|
|
362
520
|
started_at: startedAt.toISOString(),
|
|
363
521
|
duration_seconds: durationSeconds,
|
|
364
|
-
}
|
|
522
|
+
};
|
|
523
|
+
if (workerUsage && typeof workerUsage.result === 'string') {
|
|
524
|
+
record.result = workerUsage.result.toLowerCase();
|
|
525
|
+
}
|
|
526
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
365
527
|
await appendUsage(file, line);
|
|
366
528
|
}
|
|
367
529
|
if (workerError) throw workerError;
|
|
@@ -374,17 +536,42 @@ async function runCiRunner(options, dependencies = {}) {
|
|
|
374
536
|
const token = (dependencies.resolveGithubToken || resolveGithubToken)();
|
|
375
537
|
const runnerDir = await (dependencies.ensureRunner || ensureRunner)({ log: dependencies.log });
|
|
376
538
|
const host = String((dependencies.hostname || os.hostname)()).toLowerCase().replace(/[^a-z0-9-]+/g, '-');
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
539
|
+
const log = dependencies.log || console.log;
|
|
540
|
+
const prepare = dependencies.ensureRepoRunnerDirectory || ensureRepoRunnerDirectory;
|
|
541
|
+
const startLoop = dependencies.runJobLoop || runJobLoop;
|
|
542
|
+
const manyRepos = options.repos.length > 1;
|
|
543
|
+
const loops = options.repos.map((repo, index) => (async () => {
|
|
544
|
+
const repoRunnerDir = await prepare(runnerDir, repo);
|
|
545
|
+
const repoLog = manyRepos ? (line) => log(`${repo.slug}: ${line}`) : log;
|
|
546
|
+
return startLoop({
|
|
547
|
+
...options,
|
|
548
|
+
repo,
|
|
549
|
+
token,
|
|
550
|
+
runnerDir: repoRunnerDir,
|
|
551
|
+
runnerName: `atris-${host || 'worker'}-${process.pid}-${index + 1}`,
|
|
552
|
+
logPrefix: manyRepos ? `${repo.slug}: ` : '',
|
|
553
|
+
}, { ...dependencies, log: repoLog });
|
|
554
|
+
})());
|
|
555
|
+
const settled = await Promise.allSettled(loops);
|
|
556
|
+
const failures = settled
|
|
557
|
+
.map((outcome, index) => ({ outcome, repo: options.repos[index] }))
|
|
558
|
+
.filter(({ outcome }) => outcome.status === 'rejected');
|
|
559
|
+
for (const failure of failures) {
|
|
560
|
+
const detail = String(failure.outcome.reason?.message || failure.outcome.reason || 'runner loop failed')
|
|
561
|
+
.replace(/\s+/g, ' ')
|
|
562
|
+
.trim();
|
|
563
|
+
log(`${failure.repo.slug}: ${detail.toLowerCase()}`);
|
|
564
|
+
}
|
|
565
|
+
if (failures.length > 0) {
|
|
566
|
+
throw new Error(`${failures.length} ci runner ${failures.length === 1 ? 'loop' : 'loops'} failed`);
|
|
567
|
+
}
|
|
568
|
+
return settled.reduce((total, outcome) => total + outcome.value, 0);
|
|
383
569
|
}
|
|
384
570
|
|
|
385
571
|
module.exports = {
|
|
386
572
|
buildJitConfigRequest,
|
|
387
573
|
formatUsageSummary,
|
|
574
|
+
parseRunnerMarker,
|
|
388
575
|
parseRunnerArgs,
|
|
389
576
|
parseUsageArgs,
|
|
390
577
|
readUsageRecords,
|
package/lib/engine-ask.js
CHANGED
|
@@ -9,7 +9,11 @@ const {
|
|
|
9
9
|
DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
10
10
|
RUNNER_PROFILE_DEFS,
|
|
11
11
|
} = require('./runner-command');
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
canonicalEngineName,
|
|
14
|
+
engineFailureHealthStatus,
|
|
15
|
+
setEngineHealth,
|
|
16
|
+
} = require('./engine-registry');
|
|
13
17
|
const {
|
|
14
18
|
appendEngineLiveLogChunk,
|
|
15
19
|
createEngineLiveLog,
|
|
@@ -452,6 +456,15 @@ function answerStatus(answer) {
|
|
|
452
456
|
return engineTerminalStatus(answer);
|
|
453
457
|
}
|
|
454
458
|
|
|
459
|
+
function recordEngineAskHealth(answers, root) {
|
|
460
|
+
for (const answer of answers) {
|
|
461
|
+
const status = answer.ok
|
|
462
|
+
? 'ready'
|
|
463
|
+
: engineFailureHealthStatus({ ...answer, status: 'errored' });
|
|
464
|
+
if (status) setEngineHealth(answer.engine, status, root);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
455
468
|
function engineAskReceipt(answers, { concurrency, timeoutMs, at = new Date().toISOString() }) {
|
|
456
469
|
const receiptAnswers = answers.map((answer) => ({ ...answer, status: answerStatus(answer) }));
|
|
457
470
|
const answered = receiptAnswers.filter((answer) => answer.status === 'answered').length;
|
|
@@ -582,6 +595,7 @@ async function runEngineAskCommand(args, root = process.cwd(), deps = {}) {
|
|
|
582
595
|
signal: abort.signal,
|
|
583
596
|
onOutputChunk: (chunk, stream) => appendLiveLog(liveLogPath, chunk, stream),
|
|
584
597
|
});
|
|
598
|
+
recordEngineAskHealth(answers, root);
|
|
585
599
|
const receipt = engineAskReceipt(answers, {
|
|
586
600
|
concurrency: parsed.concurrency,
|
|
587
601
|
timeoutMs: parsed.timeoutMs,
|
package/lib/engine-registry.js
CHANGED
|
@@ -16,6 +16,27 @@ const ENGINE_ROLES = Object.freeze(['navigator', 'executor', 'validator']);
|
|
|
16
16
|
const ENGINE_DUTIES = Object.freeze(['leader', 'errands', 'learning']);
|
|
17
17
|
const ENGINE_HEALTH_STATUSES = Object.freeze(['ready', 'not_installed', 'credit_out', 'error']);
|
|
18
18
|
|
|
19
|
+
function engineFailureHealthStatus(result) {
|
|
20
|
+
if (!result || result.status !== 'errored') return null;
|
|
21
|
+
const signalText = [
|
|
22
|
+
result.reason,
|
|
23
|
+
result.model_unavailable,
|
|
24
|
+
result.report,
|
|
25
|
+
result.stdout,
|
|
26
|
+
result.stderr,
|
|
27
|
+
result.error,
|
|
28
|
+
result.claude && result.claude.summary,
|
|
29
|
+
result.claude && result.claude.receipt_text,
|
|
30
|
+
result.claude && result.claude.stderr,
|
|
31
|
+
result.rate_limit_info && JSON.stringify(result.rate_limit_info),
|
|
32
|
+
].filter(Boolean).join('\n').toLowerCase();
|
|
33
|
+
if (/usage[ _-]?limit|purchase more credits|insufficient credits|credit(?:s)?[ _-]?(?:out|limit)|rate[ _-]?limit|not authenticated|please log in|login required|auth(?:entication)?[ _-]?expired|payment required|subscription/.test(signalText)) {
|
|
34
|
+
return 'credit_out';
|
|
35
|
+
}
|
|
36
|
+
if (/timeout|model-unavailable/.test(signalText)) return 'not_installed';
|
|
37
|
+
return 'error';
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const ENGINE_SEED_META = Object.freeze({
|
|
20
41
|
'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), models: Object.freeze(['atris fast']), duty: 'learning', fallback_order: 10 }),
|
|
21
42
|
codex: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['codex']), fallback_order: 10 }),
|
|
@@ -356,6 +377,7 @@ module.exports = {
|
|
|
356
377
|
resolveEngineForRoleRanked,
|
|
357
378
|
resolveEngineForRole,
|
|
358
379
|
resolveEngineForRoleWithPreference,
|
|
380
|
+
engineFailureHealthStatus,
|
|
359
381
|
setEngineOverrides,
|
|
360
382
|
setEngineHealth,
|
|
361
383
|
};
|
package/lib/fleet.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
worktreeBaseRef,
|
|
24
24
|
} = require('./brief-ledger');
|
|
25
25
|
const { RUNNER_PROFILE_DEFS, buildRunnerCommand } = require('./runner-command');
|
|
26
|
+
const { engineFailureHealthStatus, setEngineHealth } = require('./engine-registry');
|
|
26
27
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
27
28
|
const { rankEnginesDetailed } = require('./router-brain');
|
|
28
29
|
const {
|
|
@@ -560,6 +561,18 @@ function detectDeadEngineDispatch(result) {
|
|
|
560
561
|
return { reason: 'nonzero_exit', exitCode };
|
|
561
562
|
}
|
|
562
563
|
|
|
564
|
+
function recordDispatchEngineHealth(result, failure, root) {
|
|
565
|
+
if (!result || !result.engine) return null;
|
|
566
|
+
const status = failure
|
|
567
|
+
? engineFailureHealthStatus({
|
|
568
|
+
...result,
|
|
569
|
+
status: 'errored',
|
|
570
|
+
reason: [result.reason, failure.reason].filter(Boolean).join('\n'),
|
|
571
|
+
})
|
|
572
|
+
: 'ready';
|
|
573
|
+
return status ? setEngineHealth(result.engine, status, root) : null;
|
|
574
|
+
}
|
|
575
|
+
|
|
563
576
|
function normalizeInstalledEngines(engines) {
|
|
564
577
|
return [...new Set((engines || [])
|
|
565
578
|
.map((entry) => (typeof entry === 'string' ? entry : entry && entry.name))
|
|
@@ -672,6 +685,7 @@ async function dispatchEntryWithRestaff({
|
|
|
672
685
|
|
|
673
686
|
const first = await runOnce(engine);
|
|
674
687
|
const deadEngine = detectDeadEngineDispatch(first);
|
|
688
|
+
recordDispatchEngineHealth(first, deadEngine, root);
|
|
675
689
|
if (!deadEngine) return first;
|
|
676
690
|
stampDispatchBrief(root, first.brief_id, 'fail', `restaffed from ${engine}: ${deadEngine.reason}`);
|
|
677
691
|
|
|
@@ -687,6 +701,7 @@ async function dispatchEntryWithRestaff({
|
|
|
687
701
|
|
|
688
702
|
restaffState.used = true;
|
|
689
703
|
const fallbackResult = await runOnce(fallback);
|
|
704
|
+
recordDispatchEngineHealth(fallbackResult, detectDeadEngineDispatch(fallbackResult), root);
|
|
690
705
|
return {
|
|
691
706
|
...fallbackResult,
|
|
692
707
|
restaffed: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.48.0",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"commands/",
|
|
15
15
|
"decks/",
|
|
16
16
|
"scripts/agent_worktree.py",
|
|
17
|
+
"scripts/det/",
|
|
17
18
|
"utils/",
|
|
18
19
|
"lib/",
|
|
19
20
|
"templates/",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Deterministic task scripts
|
|
2
|
+
|
|
3
|
+
Small, zero-dependency scripts for jobs LLMs get asked to do constantly but that
|
|
4
|
+
are actually deterministic: extracting, converting, counting, reformatting text,
|
|
5
|
+
and drafting commit/PR text from git. A cheap model (or a human, or a cron) runs
|
|
6
|
+
the script instead of spending tokens and risking a wrong guess. The output is
|
|
7
|
+
exact and reproducible, not inferred.
|
|
8
|
+
|
|
9
|
+
## Pick a tool (one read)
|
|
10
|
+
|
|
11
|
+
Find the row that matches the ask, run the command. All paths are under
|
|
12
|
+
`node scripts/det/`. Add `--json` to any script for structured output.
|
|
13
|
+
|
|
14
|
+
| If the ask is… | Run | Modes / notes |
|
|
15
|
+
|----------------|-----|---------------|
|
|
16
|
+
| pull links / emails / code / numbers out of text | `extract.js <mode> < in` | `urls` `emails` `code` `numbers` `ipv4` `hashtags` |
|
|
17
|
+
| reformat / validate / flatten JSON, or JSON to CSV | `json.js <mode> < in` | `pretty` `min` `validate` `keys` `csv` |
|
|
18
|
+
| dedupe / sort / count / slugify / trim lines | `text.js <mode> < in` | `dedupe` `sort` `rsort` `count` `slug` `trim` |
|
|
19
|
+
| base64 / hex encode-decode, sha256 / sha1 / md5 hash | `hash.js <mode> < in` | `b64` `b64d` `sha256` `sha1` `md5` `hexenc` `hexdec` |
|
|
20
|
+
| convert a timestamp, or get the weekday (all UTC) | `date.js <mode> < in` | `iso` `epoch` `epochms` `weekday` |
|
|
21
|
+
| write a commit message | `git add -A && commit-msg.js` | reads the staged diff |
|
|
22
|
+
| summarize what changed since a release | `changelog.js [ref]` | reads git log |
|
|
23
|
+
| write a PR description for this branch | `pr-description.js [base]` | reads the branch diff |
|
|
24
|
+
|
|
25
|
+
If no row matches, do the task normally. This library grows one verified script
|
|
26
|
+
at a time; never add one without a self-test.
|
|
27
|
+
|
|
28
|
+
## How to call
|
|
29
|
+
|
|
30
|
+
The first five read stdin, write stdout, exit 0 on success and non-zero on bad
|
|
31
|
+
input. The last three read git directly (their input is the repo, not stdin).
|
|
32
|
+
|
|
33
|
+
You can call any script directly, or use the dispatcher as a discovery front door:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node scripts/det/det.js # print the catalog (all 8 tools)
|
|
37
|
+
node scripts/det/det.js <script> <mode> < input # route stdin through it
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`det.js` lists every tool: the five stdin scripts it can route, plus the three
|
|
41
|
+
git-facing scripts (which it points you to run directly, since their input is the
|
|
42
|
+
repo). The stdin catalog is derived from the scripts' own exports, so it can never
|
|
43
|
+
drift from what actually runs. Trust the output; do not "improve" it.
|
|
44
|
+
|
|
45
|
+
## stdin scripts
|
|
46
|
+
|
|
47
|
+
### extract.js
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cat page.html | node scripts/det/extract.js urls
|
|
51
|
+
node scripts/det/extract.js emails < contacts.txt
|
|
52
|
+
node scripts/det/extract.js code < README.md # fenced blocks, contents only
|
|
53
|
+
node scripts/det/extract.js --json urls < page.html # JSON array
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Duplicates removed, first-seen order preserved. Unknown mode exits 2.
|
|
57
|
+
|
|
58
|
+
### json.js
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cat data.json | node scripts/det/json.js pretty # 2-space indent
|
|
62
|
+
node scripts/det/json.js min < data.json # minified
|
|
63
|
+
node scripts/det/json.js validate < data.json # "valid" or errors (exit 2)
|
|
64
|
+
node scripts/det/json.js keys < data.json # top-level keys
|
|
65
|
+
node scripts/det/json.js csv < array.json # array of objects -> RFC-4180 CSV
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`csv` handles the escaping LLMs get wrong: fields with commas or quotes are
|
|
69
|
+
quoted, inner quotes doubled. Columns follow first-seen key order across rows.
|
|
70
|
+
|
|
71
|
+
### text.js
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cat list.txt | node scripts/det/text.js dedupe # drop dup lines, keep first order
|
|
75
|
+
node scripts/det/text.js sort < list.txt # byte-order sort (rsort = reverse)
|
|
76
|
+
node scripts/det/text.js count < list.txt # lines / words / chars (tab-separated)
|
|
77
|
+
node scripts/det/text.js slug < titles.txt # each line -> url slug (accents folded)
|
|
78
|
+
node scripts/det/text.js trim < messy.txt # strip trailing ws, drop blank lines
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`count` is exact, no more eyeballed line/word totals. `slug` folds accents
|
|
82
|
+
(Café to cafe) so slugs are stable across inputs.
|
|
83
|
+
|
|
84
|
+
### hash.js
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
printf 'hi' | node scripts/det/hash.js b64 # base64 encode (b64d decodes)
|
|
88
|
+
node scripts/det/hash.js sha256 < file.txt # real hex sha256 (sha1, md5 too)
|
|
89
|
+
node scripts/det/hash.js hexenc < file.txt # raw <-> hex (hexdec reverses)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A single trailing newline is stripped before encoding/hashing, so `echo hi` and
|
|
93
|
+
`printf 'hi'` give the same result. These are real crypto digests, not the
|
|
94
|
+
plausible-looking fakes an LLM emits.
|
|
95
|
+
|
|
96
|
+
### date.js
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
echo 1700000000 | node scripts/det/date.js iso # epoch (s or ms) -> ISO UTC
|
|
100
|
+
echo 2026-07-07 | node scripts/det/date.js epoch # date -> epoch seconds (epochms for ms)
|
|
101
|
+
echo 2026-07-07 | node scripts/det/date.js weekday # -> Tuesday
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Everything is UTC and machine-independent: epoch auto-detects seconds vs ms, and
|
|
105
|
+
a bare date string with no timezone is pinned to UTC instead of guessing local.
|
|
106
|
+
|
|
107
|
+
## git-facing scripts
|
|
108
|
+
|
|
109
|
+
These replace LLM *generation*, not just data munging. They read git directly, so
|
|
110
|
+
there is no stdin and they sit outside the dispatcher catalog.
|
|
111
|
+
|
|
112
|
+
### commit-msg.js
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git add -A && node scripts/det/commit-msg.js # print the drafted message
|
|
116
|
+
node scripts/det/commit-msg.js --json # {type,scope,subject,body,...}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Type and scope come from the changed paths (`docs`/`test`/`chore`/`feat`/`fix`,
|
|
120
|
+
scope = deepest common dir); the body is exact diff stats. No intent-guessing.
|
|
121
|
+
Multi-file changes name the lead file (the added one, else the biggest churn), as
|
|
122
|
+
`add changelog.js (+2 more)`, never the vague `update 3 files`.
|
|
123
|
+
|
|
124
|
+
### changelog.js
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node scripts/det/changelog.js # since the last tag -> markdown
|
|
128
|
+
node scripts/det/changelog.js v3.34.0 # since a specific ref
|
|
129
|
+
node scripts/det/changelog.js v3.34.0 HEAD # explicit range
|
|
130
|
+
node scripts/det/changelog.js --json # {sections,counts,breaking,...}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Sections, order, and bullets come straight from the commit subjects grouped by
|
|
134
|
+
Conventional-Commits type (`feat` to Features, `fix` to Fixes, ...); `type!:`
|
|
135
|
+
commits surface under BREAKING CHANGES. Subjects that don't match the header
|
|
136
|
+
grammar land in "Other" so nothing is dropped. No paraphrase, no invented or
|
|
137
|
+
missing entries.
|
|
138
|
+
|
|
139
|
+
### pr-description.js
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
node scripts/det/pr-description.js # diff origin/master...HEAD -> markdown
|
|
143
|
+
node scripts/det/pr-description.js origin/main # different base branch
|
|
144
|
+
node scripts/det/pr-description.js origin/main HEAD # explicit base + head
|
|
145
|
+
node scripts/det/pr-description.js --json # {title,summary,testPlan,...}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Title comes from the commits (one commit -> its subject; many -> dominant type
|
|
149
|
+
plus lead file); the summary is one bullet per changed area with counts and
|
|
150
|
+
churn; the test-plan lists the touched test files plus one check per non-test
|
|
151
|
+
area. Every line is backed by a real change in the diff, no invented rationale.
|
|
152
|
+
|
|
153
|
+
## Verifying the library
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
node scripts/det/test.js # runs every script against known input/output
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Runs fast, no deps, CI-safe. A script is not "done" until it appears here with a
|
|
160
|
+
passing test. This suite is also gated by the repo's `npm test` via
|
|
161
|
+
`test/det.test.js`, which runs it as a subprocess, so the library cannot silently
|
|
162
|
+
rot in CI.
|