atris 3.76.0 → 3.78.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/README.md +2 -0
- package/commands/brief.js +219 -36
- package/commands/engine.js +14 -4
- package/commands/mission.js +23 -2
- package/commands/rating.js +7 -10
- package/lib/builder-pick.js +888 -0
- package/lib/claude-spend-guard.js +24 -0
- package/lib/fleet.js +135 -19
- package/lib/gm-season.js +110 -4
- package/lib/landing-check.js +296 -0
- package/lib/owner-brief.js +1060 -0
- package/lib/runner-command.js +78 -38
- package/lib/task-db.js +35 -3
- package/package.json +1 -1
package/lib/runner-command.js
CHANGED
|
@@ -191,20 +191,23 @@ function shellWord(value) {
|
|
|
191
191
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
// Every reader below takes the environment it reads from and defaults to this process's. A launch is built in
|
|
195
|
+
// this process, so it reads process.env; a caller that wants to know what a launch would carry (launchFor) hands
|
|
196
|
+
// in the environment the launch would be built in, and nothing here looks at this process then.
|
|
197
|
+
function firstConfiguredEnv(names, env = process.env) {
|
|
195
198
|
for (const name of names) {
|
|
196
|
-
const value = String(
|
|
199
|
+
const value = String(env[name] || '').trim();
|
|
197
200
|
if (value) return value;
|
|
198
201
|
}
|
|
199
202
|
return '';
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
function resolveRunnerProfileName() {
|
|
203
|
-
return String(
|
|
205
|
+
function resolveRunnerProfileName(env = process.env) {
|
|
206
|
+
return String(env.ATRIS_RUNNER_PROFILE || '').trim();
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
function resolveRunnerProfile() {
|
|
207
|
-
const name = resolveRunnerProfileName();
|
|
209
|
+
function resolveRunnerProfile(env = process.env) {
|
|
210
|
+
const name = resolveRunnerProfileName(env);
|
|
208
211
|
if (!name) return null;
|
|
209
212
|
const profile = RUNNER_PROFILES[name];
|
|
210
213
|
if (!profile) {
|
|
@@ -213,8 +216,8 @@ function resolveRunnerProfile() {
|
|
|
213
216
|
return profile;
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
function runnerProfileValue(key) {
|
|
217
|
-
const profile = resolveRunnerProfile();
|
|
219
|
+
function runnerProfileValue(key, env = process.env) {
|
|
220
|
+
const profile = resolveRunnerProfile(env);
|
|
218
221
|
return profile && profile[key] ? profile[key] : '';
|
|
219
222
|
}
|
|
220
223
|
|
|
@@ -233,10 +236,10 @@ function isSpendGatedModel(model) {
|
|
|
233
236
|
|
|
234
237
|
// `fallback` is the model to run instead. An engine that is not the claude cli
|
|
235
238
|
// cannot take a claude model id, so it passes '' and rides its own default.
|
|
236
|
-
function applySpendGate(model, { silent = false, fallback = SPEND_FALLBACK_MODEL } = {}) {
|
|
239
|
+
function applySpendGate(model, { silent = false, fallback = SPEND_FALLBACK_MODEL, env = process.env } = {}) {
|
|
237
240
|
const value = String(model || '').trim();
|
|
238
241
|
if (!value || !isSpendGatedModel(value)) return value;
|
|
239
|
-
if (String(
|
|
242
|
+
if (String(env.ATRIS_CLAUDE_SPEND_OK || '').trim() === value) return value;
|
|
240
243
|
if (!silent && !spendGateWarned.has(`${value}>${fallback}`)) {
|
|
241
244
|
spendGateWarned.add(`${value}>${fallback}`);
|
|
242
245
|
const instead = fallback || "that engine's own default model";
|
|
@@ -251,37 +254,37 @@ function resetSpendGateWarnings() {
|
|
|
251
254
|
|
|
252
255
|
// Engines that run only their own vendor's models drop any claude model name
|
|
253
256
|
// later, so the gate has nothing to stop there and stays quiet.
|
|
254
|
-
function runnerDropsClaudeModels() {
|
|
255
|
-
const name = resolveRunnerProfileName();
|
|
257
|
+
function runnerDropsClaudeModels(env = process.env) {
|
|
258
|
+
const name = resolveRunnerProfileName(env);
|
|
256
259
|
const def = name ? RUNNER_PROFILE_DEFS[RUNNER_PROFILE_ALIASES[name] || name] : null;
|
|
257
260
|
if (def && def.ownModelsOnly) return true;
|
|
258
|
-
return Boolean(ownModelsOnlyEngineForBin(resolveClaudeRunnerBin()));
|
|
261
|
+
return Boolean(ownModelsOnlyEngineForBin(resolveClaudeRunnerBin(env)));
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
// Whether the launch goes through the claude cli, the only engine that can take
|
|
262
265
|
// the cheap claude fallback by name.
|
|
263
|
-
function runnerRidesClaudeCli() {
|
|
264
|
-
const name = resolveRunnerProfileName();
|
|
266
|
+
function runnerRidesClaudeCli(env = process.env) {
|
|
267
|
+
const name = resolveRunnerProfileName(env);
|
|
265
268
|
const canonical = name ? (RUNNER_PROFILE_ALIASES[name] || name) : '';
|
|
266
269
|
return !canonical || canonical === 'claude' || canonical === 'fable' || canonical === 'haiku';
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
// The choke point: every unattended Claude launch (mission, autopilot, run,
|
|
270
273
|
// pulse, fleet) resolves its model here, directly or via buildRunnerCommand.
|
|
271
|
-
function resolveClaudeRunnerModel(mission) {
|
|
272
|
-
const model = resolveClaudeRunnerModelUngated(mission);
|
|
273
|
-
if (!isSpendGatedModel(model) || runnerDropsClaudeModels()) return model;
|
|
274
|
-
return applySpendGate(model, { fallback: runnerRidesClaudeCli() ? SPEND_FALLBACK_MODEL : '' });
|
|
274
|
+
function resolveClaudeRunnerModel(mission, { env = process.env, silent = false } = {}) {
|
|
275
|
+
const model = resolveClaudeRunnerModelUngated(mission, env);
|
|
276
|
+
if (!isSpendGatedModel(model) || runnerDropsClaudeModels(env)) return model;
|
|
277
|
+
return applySpendGate(model, { fallback: runnerRidesClaudeCli(env) ? SPEND_FALLBACK_MODEL : '', env, silent });
|
|
275
278
|
}
|
|
276
279
|
|
|
277
|
-
function resolveClaudeRunnerModelUngated(mission) {
|
|
280
|
+
function resolveClaudeRunnerModelUngated(mission, env = process.env) {
|
|
278
281
|
const explicit = mission && mission.model != null ? normalizeClaudeModelName(mission.model) : '';
|
|
279
282
|
if (explicit) return explicit;
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
const profileModel = normalizeClaudeModelName(runnerProfileValue('model'));
|
|
283
|
+
const fromEnv = normalizeClaudeModelName(firstConfiguredEnv(['ATRIS_RUNNER_MODEL'], env));
|
|
284
|
+
if (fromEnv) return fromEnv;
|
|
285
|
+
const profileModel = normalizeClaudeModelName(runnerProfileValue('model', env));
|
|
283
286
|
if (profileModel) return profileModel;
|
|
284
|
-
const legacyEnv = normalizeClaudeModelName(firstConfiguredEnv(['ATRIS_CLAUDE_MODEL']));
|
|
287
|
+
const legacyEnv = normalizeClaudeModelName(firstConfiguredEnv(['ATRIS_CLAUDE_MODEL'], env));
|
|
285
288
|
if (legacyEnv) return legacyEnv;
|
|
286
289
|
return DEFAULT_CLAUDE_RUNNER_MODEL;
|
|
287
290
|
}
|
|
@@ -290,12 +293,12 @@ function resolveClaudeRunnerModelUngated(mission) {
|
|
|
290
293
|
// mission model) or the profile's own pin. ATRIS_RUNNER_MODEL is left out on
|
|
291
294
|
// purpose: it usually names a claude model, which devin or grok would reject.
|
|
292
295
|
// '' means nothing is pinned, so the engine's CLI picks its own default.
|
|
293
|
-
function resolvePinnedRunnerModel(mission) {
|
|
296
|
+
function resolvePinnedRunnerModel(mission, { env = process.env, silent = false } = {}) {
|
|
294
297
|
const explicit = mission && mission.model != null ? String(mission.model).trim() : '';
|
|
295
|
-
const pinned = explicit || runnerProfileValue('model');
|
|
298
|
+
const pinned = explicit || runnerProfileValue('model', env);
|
|
296
299
|
// Cheap check first: an ungated pin never needs the profile looked up.
|
|
297
|
-
if (!isSpendGatedModel(pinned) || runnerDropsClaudeModels()) return pinned;
|
|
298
|
-
return applySpendGate(pinned, { fallback: runnerRidesClaudeCli() ? SPEND_FALLBACK_MODEL : '' });
|
|
300
|
+
if (!isSpendGatedModel(pinned) || runnerDropsClaudeModels(env)) return pinned;
|
|
301
|
+
return applySpendGate(pinned, { fallback: runnerRidesClaudeCli(env) ? SPEND_FALLBACK_MODEL : '', env, silent });
|
|
299
302
|
}
|
|
300
303
|
|
|
301
304
|
// The own-models-only engine a runner bin launches ("codex", "/opt/bin/grok"),
|
|
@@ -326,22 +329,22 @@ function runnerModelFor(engineId, model) {
|
|
|
326
329
|
return isClaudeFamilyModel(value) ? '' : value;
|
|
327
330
|
}
|
|
328
331
|
|
|
329
|
-
function resolveClaudeRunnerBin() {
|
|
330
|
-
const
|
|
331
|
-
if (
|
|
332
|
-
const profileBin = runnerProfileValue('bin');
|
|
332
|
+
function resolveClaudeRunnerBin(env = process.env) {
|
|
333
|
+
const fromEnv = firstConfiguredEnv(['ATRIS_RUNNER_BIN'], env);
|
|
334
|
+
if (fromEnv) return fromEnv;
|
|
335
|
+
const profileBin = runnerProfileValue('bin', env);
|
|
333
336
|
if (profileBin) return profileBin;
|
|
334
|
-
const legacyEnv = firstConfiguredEnv(['ATRIS_CLAUDE_BIN']);
|
|
337
|
+
const legacyEnv = firstConfiguredEnv(['ATRIS_CLAUDE_BIN'], env);
|
|
335
338
|
if (legacyEnv) return legacyEnv;
|
|
336
339
|
return DEFAULT_CLAUDE_RUNNER_BIN;
|
|
337
340
|
}
|
|
338
341
|
|
|
339
|
-
function resolveClaudeRunnerCommandTemplate() {
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
342
|
-
const profileTemplate = runnerProfileValue('commandTemplate');
|
|
342
|
+
function resolveClaudeRunnerCommandTemplate(env = process.env) {
|
|
343
|
+
const fromEnv = firstConfiguredEnv(['ATRIS_RUNNER_COMMAND_TEMPLATE'], env);
|
|
344
|
+
if (fromEnv) return fromEnv;
|
|
345
|
+
const profileTemplate = runnerProfileValue('commandTemplate', env);
|
|
343
346
|
if (profileTemplate) return profileTemplate;
|
|
344
|
-
return firstConfiguredEnv(['ATRIS_CLAUDE_COMMAND_TEMPLATE']);
|
|
347
|
+
return firstConfiguredEnv(['ATRIS_CLAUDE_COMMAND_TEMPLATE'], env);
|
|
345
348
|
}
|
|
346
349
|
|
|
347
350
|
function buildRunnerAvailabilityCommand() {
|
|
@@ -453,6 +456,42 @@ function buildRunnerCommand({ promptFile, allowedTools, model, effort } = {}) {
|
|
|
453
456
|
return cmd;
|
|
454
457
|
}
|
|
455
458
|
|
|
459
|
+
// What the launch of one engine would carry, decided from `env` (this process's by default) and without launching
|
|
460
|
+
// anything or saying anything: { engine, bin, model }, or null for an engine nobody knows. `model` is the pin a
|
|
461
|
+
// caller would hand buildRunnerCommand (a roster pick). The model is the --model of the command buildRunnerCommand
|
|
462
|
+
// builds for this engine under ATRIS_RUNNER_PROFILE=<engine>, the way the fleet builds it: the same precedence,
|
|
463
|
+
// the same spend gate, and a claude model dropped by an engine that cannot take one. '' means the launch carries
|
|
464
|
+
// none. A caller that must judge the launch (the builder picker asks the Claude spend guard about it) asks here
|
|
465
|
+
// and never works the model out for itself.
|
|
466
|
+
//
|
|
467
|
+
// Only the fleet's own launch is read. A command template somebody wrote (ATRIS_RUNNER_COMMAND_TEMPLATE, or the
|
|
468
|
+
// older ATRIS_CLAUDE_COMMAND_TEMPLATE) can start anything with anything: a model in it may be a flag, a word of the
|
|
469
|
+
// prompt, another command's, or one the shell works out only when it runs. It is never read for its model. The
|
|
470
|
+
// answer is { engine, bin, model: '', custom: true }, and a caller that has to judge it counts the model as one it
|
|
471
|
+
// cannot tell. A template that is the engine's own is the fleet's launch like any other.
|
|
472
|
+
function launchFor(engineId, { env = process.env, model } = {}) {
|
|
473
|
+
const asked = String(engineId || '').trim().toLowerCase();
|
|
474
|
+
const name = RUNNER_PROFILE_ALIASES[asked] || asked;
|
|
475
|
+
if (!name || !RUNNER_PROFILE_DEFS[name]) return null;
|
|
476
|
+
const scoped = { ...env, ATRIS_RUNNER_PROFILE: name };
|
|
477
|
+
const bin = resolveClaudeRunnerBin(scoped);
|
|
478
|
+
const template = resolveClaudeRunnerCommandTemplate(scoped);
|
|
479
|
+
if (template && template !== runnerProfileValue('commandTemplate', scoped)) {
|
|
480
|
+
return { engine: name, bin, model: '', custom: true };
|
|
481
|
+
}
|
|
482
|
+
const mission = { model };
|
|
483
|
+
const binEngine = ownModelsOnlyEngineForBin(bin);
|
|
484
|
+
const modelFor = (value) => {
|
|
485
|
+
const forProfile = runnerModelFor(name, value);
|
|
486
|
+
return binEngine ? runnerModelFor(binEngine, forProfile) : forProfile;
|
|
487
|
+
};
|
|
488
|
+
const hasModelSlot = !template || /\{(?:model|modelFlag)\}/.test(template);
|
|
489
|
+
const hasPinSlot = Boolean(template) && /\{pinnedModelFlag\}/.test(template);
|
|
490
|
+
const resolved = hasModelSlot ? resolveClaudeRunnerModel(mission, { env: scoped, silent: true }) : '';
|
|
491
|
+
const pinned = hasPinSlot ? resolvePinnedRunnerModel(mission, { env: scoped, silent: true }) : '';
|
|
492
|
+
return { engine: name, bin, model: modelFor(hasModelSlot ? resolved : pinned) };
|
|
493
|
+
}
|
|
494
|
+
|
|
456
495
|
// Whether the installed codex prints JSONL events and can write its final
|
|
457
496
|
// answer to a file. Asked once per program; an older codex without the flags
|
|
458
497
|
// keeps today's plain launch.
|
|
@@ -518,4 +557,5 @@ module.exports = {
|
|
|
518
557
|
buildRunnerAvailabilityCommand,
|
|
519
558
|
runnerAvailabilityFailureMessage,
|
|
520
559
|
buildRunnerCommand,
|
|
560
|
+
launchFor,
|
|
521
561
|
};
|
package/lib/task-db.js
CHANGED
|
@@ -645,13 +645,34 @@ const NAMED_CLAIM_SQL = `AND (? IS NULL OR EXISTS (
|
|
|
645
645
|
AND (tasks.claimed_at IS NULL OR e.created_at >= tasks.claimed_at)
|
|
646
646
|
AND e.version = (SELECT MAX(version) FROM task_events WHERE task_id = tasks.id AND event_type = 'claimed')))`;
|
|
647
647
|
|
|
648
|
+
// The job as it stands when a claim is made: its title and tags, saved on the claim itself. The builder picker
|
|
649
|
+
// (lib/builder-pick.js) gives a play the lane its job was in when the claim was made, and tagging or renaming a
|
|
650
|
+
// finished job later must not change that. Every way of claiming a job writes it (claimTask, and the claim that
|
|
651
|
+
// starting work on an open or unowned job makes); a claim made before this existed has none and is never rebuilt
|
|
652
|
+
// from the job's history.
|
|
653
|
+
function jobAtClaim(row, metadata) {
|
|
654
|
+
const meta = metadata && typeof metadata === 'object' ? metadata : {};
|
|
655
|
+
return {
|
|
656
|
+
title: String(row.title == null ? '' : row.title),
|
|
657
|
+
tag: row.tag || null,
|
|
658
|
+
tags: Array.isArray(meta.tags) ? meta.tags : [],
|
|
659
|
+
todo_tags: Array.isArray(meta.todo_tags) ? meta.todo_tags : [],
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
648
663
|
// Atomic claim. Returns { claimed: true, row, event } only if THIS call won the row; event is the claimed event
|
|
649
664
|
// this claim wrote, the one name for it (see latestClaim). Race-safe via single UPDATE with WHERE status='open'
|
|
650
665
|
// guard. SQLite serializes writes; busy_timeout absorbs contention. Caller must check `.claimed`.
|
|
651
666
|
//
|
|
652
667
|
// The row and its claimed event are written in one transaction. Before, the row was claimed first and the event
|
|
653
668
|
// followed a git call later, and in that gap a late release naming the claim before could pass for the new one.
|
|
654
|
-
|
|
669
|
+
//
|
|
670
|
+
// refuseIf is a check the caller makes on the job as it stands when the claim is made: it is asked with the open job
|
|
671
|
+
// (as getTask shows it) inside the same transaction, with the write lock held, so nothing can change the job between
|
|
672
|
+
// the look and the claim. It answers with a reason to refuse, and the job is then not claimed and nothing is written
|
|
673
|
+
// ({ claimed: false, reason }); an empty answer lets the claim go on. A job that is not open is not asked about, and
|
|
674
|
+
// answers as it always did. A check that throws takes the claim with it: nothing is claimed.
|
|
675
|
+
function claimTask(db, { id, claimedBy, leaseMinutes, leasePid, refuseIf = null }) {
|
|
655
676
|
if (!id) throw new Error('id required');
|
|
656
677
|
if (!claimedBy) throw new Error('claimedBy required');
|
|
657
678
|
const now = Date.now();
|
|
@@ -672,6 +693,11 @@ function claimTask(db, { id, claimedBy, leaseMinutes, leasePid }) {
|
|
|
672
693
|
`);
|
|
673
694
|
const lease = leaseMinutes == null ? null : JSON.stringify({ until: new Date(now + leaseMinutes * 60000).toISOString(), minutes: leaseMinutes, holder: claimedBy, pid: leasePid });
|
|
674
695
|
const won = inWriteTransaction(db, () => {
|
|
696
|
+
if (refuseIf) {
|
|
697
|
+
const current = getTask(db, id);
|
|
698
|
+
const why = current && current.status === 'open' ? refuseIf(current) : '';
|
|
699
|
+
if (why) return { refused: String(why) };
|
|
700
|
+
}
|
|
675
701
|
const result = withBusyRetry(() => stmt.run(claimedBy, now, now, lease, lease, id));
|
|
676
702
|
if (result.changes !== 1) return null;
|
|
677
703
|
let row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
@@ -693,10 +719,16 @@ function claimTask(db, { id, claimedBy, leaseMinutes, leasePid }) {
|
|
|
693
719
|
workspaceRoot: row.workspace_root,
|
|
694
720
|
actor: claimedBy,
|
|
695
721
|
eventType: 'claimed',
|
|
696
|
-
payload: {
|
|
722
|
+
payload: {
|
|
723
|
+
claimed_by: claimedBy,
|
|
724
|
+
...(startCommit ? { start_commit: startCommit } : {}),
|
|
725
|
+
// Written in this transaction, so a claim never exists without it.
|
|
726
|
+
job_at_claim: jobAtClaim(row, claimedMetadata),
|
|
727
|
+
},
|
|
697
728
|
});
|
|
698
729
|
return { row, event };
|
|
699
730
|
});
|
|
731
|
+
if (won && won.refused) return { claimed: false, reason: won.refused };
|
|
700
732
|
if (won) {
|
|
701
733
|
const enriched = { ...won.row, metadata: won.row.metadata ? safeJSON(won.row.metadata) : null };
|
|
702
734
|
// The work log is a note for people, written once the claim is saved. A note that cannot be written must not
|
|
@@ -2300,7 +2332,7 @@ function stageTaskSteps(db, {
|
|
|
2300
2332
|
workspaceRoot: updated.workspace_root,
|
|
2301
2333
|
actor: claimedWorker,
|
|
2302
2334
|
eventType: 'claimed',
|
|
2303
|
-
payload: { claimed_by: claimedWorker },
|
|
2335
|
+
payload: { claimed_by: claimedWorker, job_at_claim: jobAtClaim(updated, updated.metadata) },
|
|
2304
2336
|
});
|
|
2305
2337
|
}
|
|
2306
2338
|
const event = appendTaskEvent(db, {
|
package/package.json
CHANGED