atris 3.60.0 → 3.62.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 +37 -5
- package/atris/skills/engines/SKILL.md +9 -9
- package/bin/atris.js +1 -0
- package/commands/autoland.js +36 -5
- package/commands/autopilot.js +105 -6
- package/commands/engine.js +96 -21
- package/commands/host.js +56 -4
- package/commands/land.js +129 -8
- package/commands/member.js +33 -8
- package/commands/mission.js +112 -3
- package/commands/one-lap.js +14 -10
- package/commands/stream.js +56 -22
- package/commands/team.js +82 -10
- package/commands/worktree.js +39 -0
- package/commands/youtube.js +36 -26
- package/lib/engine-ask.js +112 -9
- package/lib/engine-registry.js +283 -39
- package/lib/fleet.js +444 -26
- package/lib/functional-owner.js +15 -6
- package/lib/host.js +311 -175
- package/lib/member-engine.js +11 -1
- package/lib/member-park.js +138 -0
- package/lib/member-scaffold.js +37 -0
- package/lib/roster-markdown.js +1 -1
- package/lib/roster-prep.js +289 -0
- package/lib/roster-runs.js +389 -0
- package/lib/roster-suggest.js +145 -0
- package/lib/router-brain.js +111 -3
- package/lib/self-drive.js +26 -2
- package/lib/state-detection.js +3 -3
- package/lib/task-db.js +38 -3
- package/lib/team-lineup.js +7 -2
- package/lib/todo.js +1 -1
- package/lib/wish-audit.js +1 -1
- package/package.json +1 -1
- package/scripts/det/cpu-hotspots.js +216 -0
- package/scripts/det/ytnotes +105 -19
- package/scripts/det/ytquote-repair.js +18 -4
- package/templates/members/host/MEMBER.md +5 -3
- package/templates/members/host/MISSION.md +1 -1
- package/templates/members/host/skills/host/SKILL.md +61 -12
package/commands/member.js
CHANGED
|
@@ -11,7 +11,7 @@ const { defaultObjectiveRunner } = require('../lib/default-runner');
|
|
|
11
11
|
const { readJson, writeJson } = require('../lib/json-file');
|
|
12
12
|
const { sleepSync } = require('../lib/sleep-sync');
|
|
13
13
|
const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
|
|
14
|
-
const { ensureMemberBundle, memberBundlePresent } = require('../lib/member-scaffold');
|
|
14
|
+
const { ensureMemberBundle, memberBundlePresent, updateMemberSkills } = require('../lib/member-scaffold');
|
|
15
15
|
const { memberProcessPrompt, MEMBER_PROCESS_PATH } = require('../lib/member-context');
|
|
16
16
|
|
|
17
17
|
function findWorkspaceBusinessId(startDir = process.cwd()) {
|
|
@@ -1078,6 +1078,13 @@ function memberRun(name, ...args) {
|
|
|
1078
1078
|
|
|
1079
1079
|
const asJson = hasFlag(args, '--json');
|
|
1080
1080
|
const paths = requireMemberDir(name);
|
|
1081
|
+
// Parking only hides a member from the team views; a run by name still goes.
|
|
1082
|
+
const { isParkedText } = require('../lib/member-park');
|
|
1083
|
+
try {
|
|
1084
|
+
if (isParkedText(fs.readFileSync(paths.memberFile, 'utf8'))) {
|
|
1085
|
+
console.error(`${name} is parked; running anyway`);
|
|
1086
|
+
}
|
|
1087
|
+
} catch { /* an unreadable card is requireMemberDir's problem, not this notice's */ }
|
|
1081
1088
|
const requestedVerifier = readFlag(args, '--verify', '') || memberRunConfiguredVerifier(paths);
|
|
1082
1089
|
const missionText = memberRunMissionText(args);
|
|
1083
1090
|
const hasMissionOverride = Boolean(readFlag(args, '--mission', '') || readFlag(args, '--mission-id', ''));
|
|
@@ -4095,6 +4102,8 @@ function memberList(...flags) {
|
|
|
4095
4102
|
const asJson = flags.includes('--json') || process.argv.includes('--json');
|
|
4096
4103
|
const teamDir = path.join(process.cwd(), 'atris', 'team');
|
|
4097
4104
|
const members = findAllMembers(teamDir);
|
|
4105
|
+
const { isMemberParked } = require('../lib/member-park');
|
|
4106
|
+
const parked = new Set(members.filter((m) => isMemberParked(process.cwd(), m.name)).map((m) => m.name));
|
|
4098
4107
|
|
|
4099
4108
|
if (asJson) {
|
|
4100
4109
|
console.log(JSON.stringify({
|
|
@@ -4102,6 +4111,7 @@ function memberList(...flags) {
|
|
|
4102
4111
|
count: members.length,
|
|
4103
4112
|
members: members.map((m) => ({
|
|
4104
4113
|
name: m.name,
|
|
4114
|
+
...(parked.has(m.name) ? { parked: true } : {}),
|
|
4105
4115
|
role: m.role,
|
|
4106
4116
|
format: m.format,
|
|
4107
4117
|
skills: m.format === 'directory' ? m.skillCount : null,
|
|
@@ -4149,12 +4159,14 @@ function memberList(...flags) {
|
|
|
4149
4159
|
clipCell(m.format === 'directory' ? 'dir' : 'flat', fmtW) +
|
|
4150
4160
|
clipCell(skills, skillW) +
|
|
4151
4161
|
clipCell(context, ctxW) +
|
|
4152
|
-
(m.version || '-')
|
|
4162
|
+
(m.version || '-') +
|
|
4163
|
+
(parked.has(m.name) ? ' parked' : '')
|
|
4153
4164
|
);
|
|
4154
4165
|
}
|
|
4155
4166
|
|
|
4156
4167
|
console.log('');
|
|
4157
|
-
|
|
4168
|
+
const parkedNote = parked.size ? ` (${parked.size} parked; unpark with atris team unpark <name>)` : '';
|
|
4169
|
+
console.log(`${members.length} ${members.length === 1 ? 'member' : 'members'} found.${parkedNote}`);
|
|
4158
4170
|
}
|
|
4159
4171
|
|
|
4160
4172
|
// --- CREATE subcommand ---
|
|
@@ -4296,10 +4308,10 @@ tools: []
|
|
|
4296
4308
|
}
|
|
4297
4309
|
|
|
4298
4310
|
function printMemberInstallUsage(stream = console.log) {
|
|
4299
|
-
stream('Usage: atris member install <slug>');
|
|
4311
|
+
stream('Usage: atris member install <slug> [--update]');
|
|
4300
4312
|
}
|
|
4301
4313
|
|
|
4302
|
-
function memberInstall(slug) {
|
|
4314
|
+
function memberInstall(slug, update = false) {
|
|
4303
4315
|
if (slug === '--help' || slug === '-h') {
|
|
4304
4316
|
printMemberInstallUsage();
|
|
4305
4317
|
return;
|
|
@@ -4314,9 +4326,22 @@ function memberInstall(slug) {
|
|
|
4314
4326
|
}
|
|
4315
4327
|
|
|
4316
4328
|
const memberDir = path.join(process.cwd(), 'atris', 'team', slug);
|
|
4329
|
+
const templateDir = path.join(__dirname, '..', 'templates', 'members', slug);
|
|
4330
|
+
if (update) {
|
|
4331
|
+
if (!fs.existsSync(templateDir)) {
|
|
4332
|
+
console.error(`No member template for ${slug}.`);
|
|
4333
|
+
process.exit(1);
|
|
4334
|
+
}
|
|
4335
|
+
const updated = updateMemberSkills(templateDir, memberDir);
|
|
4336
|
+
if (updated.length === 0) {
|
|
4337
|
+
console.log(`MEMBER ${slug} already up to date`);
|
|
4338
|
+
} else {
|
|
4339
|
+
for (const rel of updated) console.log(`atris/team/${slug}/${rel.split(path.sep).join('/')}`);
|
|
4340
|
+
}
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4317
4343
|
const already = memberBundlePresent(memberDir);
|
|
4318
4344
|
fs.mkdirSync(path.join(memberDir, 'logs'), { recursive: true });
|
|
4319
|
-
const templateDir = path.join(__dirname, '..', 'templates', 'members', slug);
|
|
4320
4345
|
const copied = [];
|
|
4321
4346
|
if (fs.existsSync(templateDir)) {
|
|
4322
4347
|
const copyMissing = (source, relative = '') => {
|
|
@@ -9332,7 +9357,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9332
9357
|
case 'new':
|
|
9333
9358
|
return memberCreate(args[0], ...args.slice(1));
|
|
9334
9359
|
case 'install':
|
|
9335
|
-
return memberInstall(args[0]);
|
|
9360
|
+
return memberInstall(args[0], args.includes('--update'));
|
|
9336
9361
|
case 'activate':
|
|
9337
9362
|
return memberActivate(args[0]);
|
|
9338
9363
|
case 'upgrade':
|
|
@@ -9394,7 +9419,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
9394
9419
|
console.log('');
|
|
9395
9420
|
console.log('Subcommands:');
|
|
9396
9421
|
console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
|
|
9397
|
-
console.log(' install <slug>
|
|
9422
|
+
console.log(' install <slug> [--update] Install missing files, or refresh template skills');
|
|
9398
9423
|
console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
|
|
9399
9424
|
console.log(' list [--json] Show all team members');
|
|
9400
9425
|
console.log(' activate <name> Symlink member skills, show context and permissions');
|
package/commands/mission.js
CHANGED
|
@@ -32,8 +32,11 @@ const {
|
|
|
32
32
|
const {
|
|
33
33
|
resolveEngineForRoleWithPreference,
|
|
34
34
|
engineFailureHealthStatus,
|
|
35
|
+
engineStallReason,
|
|
36
|
+
recordEngineRunHealth,
|
|
35
37
|
setEngineHealth,
|
|
36
38
|
} = require('../lib/engine-registry');
|
|
39
|
+
const { appendRosterRun, parseRunUsage } = require('../lib/roster-runs');
|
|
37
40
|
const {
|
|
38
41
|
FUNCTIONAL_MEMBER_TOPICS,
|
|
39
42
|
listWorkspaceMemberSlugs,
|
|
@@ -391,9 +394,29 @@ function rosterTickPins(engine) {
|
|
|
391
394
|
return {
|
|
392
395
|
...(engine && engine.roster_effort ? { roster_effort: engine.roster_effort } : {}),
|
|
393
396
|
...(engine && engine.roster_max_seconds ? { roster_max_seconds: engine.roster_max_seconds } : {}),
|
|
397
|
+
...(engine && engine.roster_prep ? { roster_prep: engine.roster_prep } : {}),
|
|
394
398
|
};
|
|
395
399
|
}
|
|
396
400
|
|
|
401
|
+
// A tick whose roster worker asks for prep ("prep: search") runs the same
|
|
402
|
+
// prep pass fleet runs before the worker starts, and the worker's prompt
|
|
403
|
+
// gets the brief. No prep line means the prompt goes out as it was.
|
|
404
|
+
async function missionTickPrep(root, { mission, runtimeMission, job, prompt, ask } = {}) {
|
|
405
|
+
const prepJob = String(runtimeMission && runtimeMission.roster_prep || '').trim();
|
|
406
|
+
if (!prepJob) return { prompt, prep: null };
|
|
407
|
+
const { runPrepPass, withPrepBrief } = require('../lib/roster-prep');
|
|
408
|
+
const prep = await runPrepPass({
|
|
409
|
+
prepJob,
|
|
410
|
+
forJob: job || 'build',
|
|
411
|
+
task: { display_id: (mission && mission.id) || '', title: (mission && (mission.objective || mission.slug)) || '' },
|
|
412
|
+
prompt,
|
|
413
|
+
root,
|
|
414
|
+
record: { member: (mission && mission.owner) || '' },
|
|
415
|
+
...(ask ? { ask } : {}),
|
|
416
|
+
});
|
|
417
|
+
return { prompt: withPrepBrief(prompt, prep), prep };
|
|
418
|
+
}
|
|
419
|
+
|
|
397
420
|
// A tick's time limit: the time left on the mission, capped by the roster
|
|
398
421
|
// line's "max 20 min" when it sets one, else by the default tick cap.
|
|
399
422
|
function missionTickTimeoutMs(mission, remainingMs) {
|
|
@@ -427,6 +450,7 @@ function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
|
427
450
|
requested_engine: null,
|
|
428
451
|
engine_fallback_reason: null,
|
|
429
452
|
member_engine_reason: member.reason,
|
|
453
|
+
roster_job: member.job || 'build',
|
|
430
454
|
};
|
|
431
455
|
}
|
|
432
456
|
}
|
|
@@ -445,10 +469,59 @@ function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
|
445
469
|
};
|
|
446
470
|
}
|
|
447
471
|
|
|
472
|
+
// A stalled tick benches its engine for the cooldown, so the next tick's
|
|
473
|
+
// roster pick is the backup; no retry inside the tick is needed.
|
|
448
474
|
function recordMissionEngineTickOutcome(engineId, result, root = process.cwd()) {
|
|
449
475
|
if (!engineId) return null;
|
|
450
|
-
|
|
451
|
-
|
|
476
|
+
if (result && result.status === 'ran') return setEngineHealth(engineId, 'ready', root);
|
|
477
|
+
if (!engineFailureHealthStatus(result)) return null;
|
|
478
|
+
return recordEngineRunHealth(engineId, result, root);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// One run-record line per engine tick: how long the worker ran, and how it
|
|
482
|
+
// ended. A tick that ran and passed (or had no verifier) landed; a failing
|
|
483
|
+
// verifier or a real error failed; a time cap or dropped connection stalled;
|
|
484
|
+
// a closed credit window is credit out. Never throws into the tick.
|
|
485
|
+
function missionTickRunOutcome(result, verifierResult, maxSeconds) {
|
|
486
|
+
if (result && result.status === 'ran') {
|
|
487
|
+
return verifierResult && verifierResult.passed === false
|
|
488
|
+
? { outcome: 'failed', detail: 'the verifier failed' }
|
|
489
|
+
: { outcome: 'landed' };
|
|
490
|
+
}
|
|
491
|
+
if (engineFailureHealthStatus(result) === 'credit_out') return { outcome: 'credit out', detail: 'the engine hit its usage limit' };
|
|
492
|
+
const stall = engineStallReason({ ...result, ...(maxSeconds ? { max_seconds: maxSeconds } : {}) });
|
|
493
|
+
if (stall) return { outcome: 'stalled', detail: stall.text };
|
|
494
|
+
if (/wall-exceeded/.test(String(result && result.reason || ''))) return { outcome: 'failed', detail: 'the mission ran out of time' };
|
|
495
|
+
const summary = String(result && result.claude && result.claude.summary || '').split('\n')[0].trim();
|
|
496
|
+
return { outcome: 'failed', detail: summary && summary !== 'error' ? summary : String(result && result.reason || 'the tick failed').replace(/-/g, ' ') };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function recordMissionTickRosterRun(root, { mission, runtimeMission, engineId, job, result, verifierResult, startedAt, endedMs, usage, prep = null } = {}) {
|
|
500
|
+
if (!engineId) return null;
|
|
501
|
+
try {
|
|
502
|
+
const { engineRunsView } = require('../lib/roster-models');
|
|
503
|
+
const { prepRecordFields } = require('../lib/roster-prep');
|
|
504
|
+
const run = runtimeMission || mission || {};
|
|
505
|
+
const startedMs = Date.parse(startedAt);
|
|
506
|
+
const maxSeconds = Number(run.roster_max_seconds) || 0;
|
|
507
|
+
return appendRosterRun(root, {
|
|
508
|
+
at: startedAt,
|
|
509
|
+
job: job || 'build',
|
|
510
|
+
member: (mission && mission.owner) || '',
|
|
511
|
+
engine: engineId,
|
|
512
|
+
model: run.model || engineRunsView(engineId).model || '',
|
|
513
|
+
effort: run.roster_effort || '',
|
|
514
|
+
max_seconds: maxSeconds,
|
|
515
|
+
seconds: Number.isFinite(startedMs) ? Math.max(0, (endedMs - startedMs) / 1000) : undefined,
|
|
516
|
+
task: (mission && mission.id) || '',
|
|
517
|
+
source: 'mission',
|
|
518
|
+
...prepRecordFields(prep),
|
|
519
|
+
...(usage || {}),
|
|
520
|
+
...missionTickRunOutcome(result, verifierResult, maxSeconds),
|
|
521
|
+
});
|
|
522
|
+
} catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
452
525
|
}
|
|
453
526
|
|
|
454
527
|
function runnerModelPatch(runner, model) {
|
|
@@ -8904,6 +8977,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8904
8977
|
stop_reason: null,
|
|
8905
8978
|
stderr: errStr,
|
|
8906
8979
|
parse_errors: 0,
|
|
8980
|
+
run_usage: parseRunUsage({ stdout, stderr }),
|
|
8907
8981
|
});
|
|
8908
8982
|
});
|
|
8909
8983
|
|
|
@@ -9008,6 +9082,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9008
9082
|
let numTurns = null;
|
|
9009
9083
|
let rateLimitInfo = null;
|
|
9010
9084
|
let stopReason = null;
|
|
9085
|
+
let usage = null;
|
|
9011
9086
|
let parseErrors = 0;
|
|
9012
9087
|
let stderr = '';
|
|
9013
9088
|
let timedOut = false;
|
|
@@ -9047,6 +9122,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9047
9122
|
if (typeof ev.duration_api_ms === 'number') durationApiMs = ev.duration_api_ms;
|
|
9048
9123
|
if (typeof ev.num_turns === 'number') numTurns = ev.num_turns;
|
|
9049
9124
|
if (ev.stop_reason) stopReason = ev.stop_reason;
|
|
9125
|
+
if (ev.usage && typeof ev.usage === 'object') usage = ev.usage;
|
|
9050
9126
|
}
|
|
9051
9127
|
} catch {
|
|
9052
9128
|
parseErrors++;
|
|
@@ -9083,6 +9159,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9083
9159
|
rate_limit_info: rateLimitInfo,
|
|
9084
9160
|
stderr: errStr,
|
|
9085
9161
|
parse_errors: parseErrors,
|
|
9162
|
+
run_usage: parseRunUsage({ usage, cost_usd: costEstimate }),
|
|
9086
9163
|
});
|
|
9087
9164
|
});
|
|
9088
9165
|
|
|
@@ -9571,6 +9648,9 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9571
9648
|
const tickStart = stampIso();
|
|
9572
9649
|
const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
|
|
9573
9650
|
let result = { status: 'skipped', reason: 'unknown', tick_index: tickIdx, ran: false, started_at: tickStart };
|
|
9651
|
+
// Set when a roster engine really ran this tick: when it stopped and the
|
|
9652
|
+
// usage it printed. The run record is written after the verifier.
|
|
9653
|
+
let tickWorkerRun = null;
|
|
9574
9654
|
let cachedStep = findCachedMissionStepReceipt(cwd, { missionId: mission.id, tickIndex: tickIdx });
|
|
9575
9655
|
let cachedVerifierResult = cachedStep ? cachedStep.verifier_result : null;
|
|
9576
9656
|
const tickSelection = resolveMissionTickRunner(runtimeMission, cwd);
|
|
@@ -9707,6 +9787,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9707
9787
|
backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
|
|
9708
9788
|
receipt_text: String(turn.text || '').slice(0, 4000),
|
|
9709
9789
|
};
|
|
9790
|
+
tickWorkerRun = { endedMs: Date.now(), usage: {} };
|
|
9710
9791
|
if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
|
|
9711
9792
|
if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
|
|
9712
9793
|
if (wallExpired) {
|
|
@@ -9724,6 +9805,17 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9724
9805
|
const pingDrain = consumeMissionPings(mission, cwd);
|
|
9725
9806
|
mission = pingDrain.mission;
|
|
9726
9807
|
runtimeMission = runtimeView(mission);
|
|
9808
|
+
let tickPrep = null;
|
|
9809
|
+
let workerPrompt = prompt;
|
|
9810
|
+
if (tickEngineId) {
|
|
9811
|
+
({ prompt: workerPrompt, prep: tickPrep } = await missionTickPrep(cwd, {
|
|
9812
|
+
mission,
|
|
9813
|
+
runtimeMission: tickRuntimeMission,
|
|
9814
|
+
job: tickSelection.roster_job || 'build',
|
|
9815
|
+
prompt,
|
|
9816
|
+
}));
|
|
9817
|
+
}
|
|
9818
|
+
const workerStartedAt = tickPrep ? stampIso() : tickStart;
|
|
9727
9819
|
const restoreTickRunnerProfile = tickEngineId ? applyMissionRunnerProfile(tickEngineId) : () => {};
|
|
9728
9820
|
let claudeResult;
|
|
9729
9821
|
let sessionBusyRetried = false;
|
|
@@ -9734,7 +9826,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9734
9826
|
tickRuntimeMission,
|
|
9735
9827
|
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
9736
9828
|
),
|
|
9737
|
-
prompt,
|
|
9829
|
+
prompt: workerPrompt,
|
|
9738
9830
|
model: resolveMissionTickRunnerModel(tickRuntimeMission),
|
|
9739
9831
|
});
|
|
9740
9832
|
try {
|
|
@@ -9754,6 +9846,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9754
9846
|
} finally {
|
|
9755
9847
|
restoreTickRunnerProfile();
|
|
9756
9848
|
}
|
|
9849
|
+
tickWorkerRun = { endedMs: Date.now(), usage: (claudeResult && claudeResult.run_usage) || {}, prep: tickPrep, startedAt: workerStartedAt };
|
|
9757
9850
|
result.claude = {
|
|
9758
9851
|
ok: claudeResult.ok,
|
|
9759
9852
|
reason: claudeResult.reason,
|
|
@@ -9860,6 +9953,20 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9860
9953
|
}
|
|
9861
9954
|
pauseReason = context.pauseReason;
|
|
9862
9955
|
context.currentTick = null;
|
|
9956
|
+
if (tickEngineId && !cachedStep && tickWorkerRun) {
|
|
9957
|
+
recordMissionTickRosterRun(cwd, {
|
|
9958
|
+
mission,
|
|
9959
|
+
runtimeMission: tickRuntimeMission,
|
|
9960
|
+
engineId: tickEngineId,
|
|
9961
|
+
job: tickSelection.roster_job || 'build',
|
|
9962
|
+
result,
|
|
9963
|
+
verifierResult,
|
|
9964
|
+
startedAt: tickWorkerRun.startedAt || tickStart,
|
|
9965
|
+
endedMs: tickWorkerRun.endedMs,
|
|
9966
|
+
usage: tickWorkerRun.usage,
|
|
9967
|
+
prep: tickWorkerRun.prep || null,
|
|
9968
|
+
});
|
|
9969
|
+
}
|
|
9863
9970
|
let receiptPath = cachedStep ? cachedStep.receipt_path : null;
|
|
9864
9971
|
|
|
9865
9972
|
// Review-lane drain: always-on loops sweep the agent-safe review actions
|
|
@@ -11792,6 +11899,8 @@ module.exports = {
|
|
|
11792
11899
|
missionTickTimeoutMs,
|
|
11793
11900
|
engineFailureHealthStatus,
|
|
11794
11901
|
recordMissionEngineTickOutcome,
|
|
11902
|
+
recordMissionTickRosterRun,
|
|
11903
|
+
missionTickPrep,
|
|
11795
11904
|
tickMadeProgress,
|
|
11796
11905
|
consecutiveNoProgressTicks,
|
|
11797
11906
|
consecutiveIdenticalSummaryTicks,
|
package/commands/one-lap.js
CHANGED
|
@@ -7,11 +7,13 @@ const { spawnSync } = require('child_process');
|
|
|
7
7
|
const { freezeMissionVerifier, listMissions, markMissionReviewReady } = require('./mission');
|
|
8
8
|
const { readWishes } = require('../lib/wish-store');
|
|
9
9
|
const {
|
|
10
|
+
engineReadyAt,
|
|
10
11
|
engineRegistryView,
|
|
11
12
|
resolveEngineForRole,
|
|
12
13
|
resolveEngineForRoleRanked,
|
|
13
14
|
resolveRegisteredEngine,
|
|
14
15
|
rosterPinFor,
|
|
16
|
+
ROSTER_PIN_FIELDS,
|
|
15
17
|
rosterDecided,
|
|
16
18
|
} = require('../lib/engine-registry');
|
|
17
19
|
const { parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
@@ -156,7 +158,7 @@ function readyExecutor(root, preferred = '') {
|
|
|
156
158
|
if (preferred) {
|
|
157
159
|
const selected = resolveRegisteredEngine(preferred, root);
|
|
158
160
|
if (!selected.roles.includes('executor')) throw new Error(`engine ${selected.id} is not an executor`);
|
|
159
|
-
if (!selected
|
|
161
|
+
if (!engineReadyAt(selected)) throw new Error(`engine ${selected.id} is not ready`);
|
|
160
162
|
if (!fleet.FLEET_CAPABLE.includes(selected.id)) throw new Error(`engine ${selected.id} cannot build headlessly`);
|
|
161
163
|
const pin = rosterPinFor('executor', selected.id, root);
|
|
162
164
|
return Object.keys(pin).length ? { ...selected, ...pin } : selected;
|
|
@@ -165,7 +167,7 @@ function readyExecutor(root, preferred = '') {
|
|
|
165
167
|
if (routed && fleet.FLEET_CAPABLE.includes(routed.id)) return routed;
|
|
166
168
|
return engineRegistryView(root)
|
|
167
169
|
.filter((engine) => engine.roles.includes('executor'))
|
|
168
|
-
.filter((engine) => engine
|
|
170
|
+
.filter((engine) => engineReadyAt(engine))
|
|
169
171
|
.filter((engine) => fleet.FLEET_CAPABLE.includes(engine.id))
|
|
170
172
|
.sort((a, b) => Number(a.fallback_order) - Number(b.fallback_order))[0] || null;
|
|
171
173
|
}
|
|
@@ -178,7 +180,7 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
178
180
|
? rosterRanked.ranked.map((engine) => engine.id)
|
|
179
181
|
: [];
|
|
180
182
|
const pinned = fromRoster && rosterRanked.engine
|
|
181
|
-
&& (rosterRanked.engine.roster_model || rosterRanked.engine.roster_effort || rosterRanked.engine.roster_max_seconds)
|
|
183
|
+
&& (rosterRanked.engine.roster_model || rosterRanked.engine.roster_effort || rosterRanked.engine.roster_max_seconds || rosterRanked.engine.roster_prep)
|
|
182
184
|
? rosterRanked.engine
|
|
183
185
|
: null;
|
|
184
186
|
// A roster pick can name an engine that reviews only when picked (codex),
|
|
@@ -196,7 +198,7 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
196
198
|
|| String(a.id).localeCompare(String(b.id));
|
|
197
199
|
})
|
|
198
200
|
.map((engine) => (pinned && engine.id === pinned.id ? { ...engine, ...pinFields(pinned) } : engine));
|
|
199
|
-
const ready = candidates.filter((engine) => engine
|
|
201
|
+
const ready = candidates.filter((engine) => engineReadyAt(engine));
|
|
200
202
|
// Route-time determinism: which validator binaries exist on this machine
|
|
201
203
|
// must not change the lap's route. When none are ready, dispatch with the
|
|
202
204
|
// policy-ordered distinct validators; the flight still enforces judge !=
|
|
@@ -207,24 +209,25 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
207
209
|
|
|
208
210
|
function pinFields(engine) {
|
|
209
211
|
const pin = {};
|
|
210
|
-
for (const field of
|
|
212
|
+
for (const field of ROSTER_PIN_FIELDS) {
|
|
211
213
|
if (engine && engine[field]) pin[field] = engine[field];
|
|
212
214
|
}
|
|
213
215
|
return pin;
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
// The roster's pins ride into the flight: the builder's model, effort,
|
|
217
|
-
//
|
|
218
|
-
// reviewer with only a model keeps the plain model string.
|
|
218
|
+
// The roster's pins ride into the flight: the builder's model, effort, time
|
|
219
|
+
// cap, and prep job for the builder, and each reviewer's for that reviewer
|
|
220
|
+
// only. A reviewer with only a model keeps the plain model string.
|
|
219
221
|
function lapModelPins(executor, validators = []) {
|
|
220
222
|
const validatorModels = {};
|
|
221
223
|
for (const engine of validators) {
|
|
222
224
|
if (!engine) continue;
|
|
223
|
-
if (engine.roster_effort || engine.roster_max_seconds) {
|
|
225
|
+
if (engine.roster_effort || engine.roster_max_seconds || engine.roster_prep) {
|
|
224
226
|
validatorModels[engine.id] = {
|
|
225
227
|
model: engine.roster_model || '',
|
|
226
228
|
effort: engine.roster_effort || '',
|
|
227
229
|
max_seconds: engine.roster_max_seconds || 0,
|
|
230
|
+
...(engine.roster_prep ? { prep: engine.roster_prep } : {}),
|
|
228
231
|
};
|
|
229
232
|
} else if (engine.roster_model) {
|
|
230
233
|
validatorModels[engine.id] = engine.roster_model;
|
|
@@ -234,6 +237,7 @@ function lapModelPins(executor, validators = []) {
|
|
|
234
237
|
model: executor && executor.roster_model || '',
|
|
235
238
|
...(executor && executor.roster_effort ? { effort: executor.roster_effort } : {}),
|
|
236
239
|
...(executor && executor.roster_max_seconds ? { maxSeconds: executor.roster_max_seconds } : {}),
|
|
240
|
+
...(executor && executor.roster_prep ? { prep: executor.roster_prep } : {}),
|
|
237
241
|
validatorModels: Object.keys(validatorModels).length ? validatorModels : null,
|
|
238
242
|
};
|
|
239
243
|
}
|
|
@@ -710,7 +714,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
710
714
|
...lapModelPins(executor, validators),
|
|
711
715
|
actor: mission.owner || 'mission-lead',
|
|
712
716
|
installedEngines: engineRegistryView(root)
|
|
713
|
-
.filter((entry) => entry
|
|
717
|
+
.filter((entry) => engineReadyAt(entry))
|
|
714
718
|
.filter((entry) => fleet.FLEET_CAPABLE.includes(entry.id))
|
|
715
719
|
.map((entry) => entry.id),
|
|
716
720
|
ownCli: runCli,
|
package/commands/stream.js
CHANGED
|
@@ -6,7 +6,7 @@ const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
|
6
6
|
const { isNonInteractive } = require('../lib/noninteractive');
|
|
7
7
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
8
8
|
const { runGit: spawnGit } = require('../lib/git-spawn');
|
|
9
|
-
const {
|
|
9
|
+
const { clearBoardMemo, sharedBoard } = require('./land');
|
|
10
10
|
const { listWorktrees } = require('./worktree');
|
|
11
11
|
|
|
12
12
|
const DEFAULT_INTERVAL_MS = 3000;
|
|
@@ -15,6 +15,11 @@ const MAX_SUMMARY = 180;
|
|
|
15
15
|
const ACTIVE_MISSION_STATES = new Set(['planning', 'active', 'running', 'ready']);
|
|
16
16
|
const ACTIVE_TASK_STATES = new Set(['claimed', 'do', 'doing', 'in_progress', 'review']);
|
|
17
17
|
|
|
18
|
+
// One stable default runner so per-process memos can key on it.
|
|
19
|
+
function defaultRunGit(root, args) {
|
|
20
|
+
return spawnGit(args, { cwd: root, check: false });
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
function defaultDeps() {
|
|
19
24
|
return {
|
|
20
25
|
existsSync: fs.existsSync,
|
|
@@ -22,7 +27,7 @@ function defaultDeps() {
|
|
|
22
27
|
readdirSync: fs.readdirSync,
|
|
23
28
|
statSync: fs.statSync,
|
|
24
29
|
watch: fs.watch,
|
|
25
|
-
runGit:
|
|
30
|
+
runGit: defaultRunGit,
|
|
26
31
|
now: () => Date.now(),
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -540,26 +545,48 @@ function collectWorkspaceRoots(root, deps) {
|
|
|
540
545
|
return [...out.values()];
|
|
541
546
|
}
|
|
542
547
|
|
|
548
|
+
// Owner and task come from one `git config` read per root, shared for a few
|
|
549
|
+
// seconds across the snapshot and the event scan of the same command.
|
|
550
|
+
const WORKTREE_CONFIG_MAX_AGE_MS = 10000;
|
|
551
|
+
const worktreeConfigMemo = new WeakMap();
|
|
552
|
+
|
|
553
|
+
function worktreeConfig(root, deps) {
|
|
554
|
+
const key = typeof deps.runGit === 'function' ? deps.runGit : null;
|
|
555
|
+
let byRoot = key ? worktreeConfigMemo.get(key) : null;
|
|
556
|
+
if (key && !byRoot) {
|
|
557
|
+
byRoot = new Map();
|
|
558
|
+
worktreeConfigMemo.set(key, byRoot);
|
|
559
|
+
}
|
|
560
|
+
const hit = byRoot && byRoot.get(root);
|
|
561
|
+
if (hit && Date.now() - hit.at < WORKTREE_CONFIG_MAX_AGE_MS) return hit;
|
|
562
|
+
const found = { owner: '', task: '', at: Date.now() };
|
|
563
|
+
const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-(owner|task)$'], deps);
|
|
564
|
+
if (result.status === 0) {
|
|
565
|
+
for (const line of String(result.stdout || '').split(/\r?\n/).filter(Boolean)) {
|
|
566
|
+
const [name, ...rest] = line.split(/\s+/);
|
|
567
|
+
const value = rest.join(' ');
|
|
568
|
+
if (!found.owner && /\.atris-owner$/i.test(name)) found.owner = value;
|
|
569
|
+
if (!found.task && /\.atris-task$/i.test(name)) found.task = value;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (byRoot) byRoot.set(root, found);
|
|
573
|
+
return found;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function clearWorktreeConfigMemo(deps) {
|
|
577
|
+
if (deps && typeof deps.runGit === 'function') worktreeConfigMemo.delete(deps.runGit);
|
|
578
|
+
}
|
|
579
|
+
|
|
543
580
|
function worktreeOwner(root, deps) {
|
|
544
581
|
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
545
582
|
if (sidecar) return sidecar.owner || sidecar.member || sidecar.agent || '';
|
|
546
|
-
|
|
547
|
-
if (result.status === 0) {
|
|
548
|
-
const line = String(result.stdout || '').split(/\r?\n/).find(Boolean);
|
|
549
|
-
if (line) return line.split(/\s+/).slice(1).join(' ');
|
|
550
|
-
}
|
|
551
|
-
return '';
|
|
583
|
+
return worktreeConfig(root, deps).owner;
|
|
552
584
|
}
|
|
553
585
|
|
|
554
586
|
function worktreeTask(root, deps) {
|
|
555
587
|
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
556
588
|
if (sidecar && sidecar.task) return sidecar.task;
|
|
557
|
-
|
|
558
|
-
if (result.status === 0) {
|
|
559
|
-
const line = String(result.stdout || '').split(/\r?\n/).find(Boolean);
|
|
560
|
-
if (line) return line.split(/\s+/).slice(1).join(' ');
|
|
561
|
-
}
|
|
562
|
-
return '';
|
|
589
|
+
return worktreeConfig(root, deps).task;
|
|
563
590
|
}
|
|
564
591
|
|
|
565
592
|
function collectWorktreeActivityEvents(root, deps, events) {
|
|
@@ -584,7 +611,7 @@ function collectWorktreeActivityEvents(root, deps, events) {
|
|
|
584
611
|
|
|
585
612
|
function collectLandingStateEvent(root, deps, events, nowMs) {
|
|
586
613
|
try {
|
|
587
|
-
const board =
|
|
614
|
+
const board = sharedBoard(root);
|
|
588
615
|
const summary = board && board.summary || {};
|
|
589
616
|
const active = Number(summary.active || 0);
|
|
590
617
|
const due = Number(summary.due || 0);
|
|
@@ -610,7 +637,7 @@ function collectLandingStateEvent(root, deps, events, nowMs) {
|
|
|
610
637
|
}
|
|
611
638
|
}
|
|
612
639
|
|
|
613
|
-
function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', nowMs = Date.now(), deps = defaultDeps() } = {}) {
|
|
640
|
+
function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', nowMs = Date.now(), deps = defaultDeps(), skipLanding = false } = {}) {
|
|
614
641
|
const allDeps = { ...defaultDeps(), ...deps };
|
|
615
642
|
const events = [];
|
|
616
643
|
const roots = collectWorkspaceRoots(root, allDeps);
|
|
@@ -622,7 +649,7 @@ function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', no
|
|
|
622
649
|
collectScorecardEvents(item.root, allDeps, events);
|
|
623
650
|
}
|
|
624
651
|
collectWorktreeActivityEvents(root, allDeps, events);
|
|
625
|
-
collectLandingStateEvent(root, allDeps, events, nowMs);
|
|
652
|
+
if (!skipLanding) collectLandingStateEvent(root, allDeps, events, nowMs);
|
|
626
653
|
const wantedAgent = agent ? sanitizeAgent(agent).toLowerCase() : '';
|
|
627
654
|
const byKey = new Map();
|
|
628
655
|
for (const event of events) {
|
|
@@ -648,7 +675,9 @@ function waitingOnOperator(tasks) {
|
|
|
648
675
|
});
|
|
649
676
|
}
|
|
650
677
|
|
|
651
|
-
|
|
678
|
+
// skipLanding: callers that never show the landing wait (the team roster)
|
|
679
|
+
// skip the landing board, the most expensive git work here.
|
|
680
|
+
function collectSnapshot({ root = process.cwd(), deps = defaultDeps(), skipLanding = false } = {}) {
|
|
652
681
|
const allDeps = { ...defaultDeps(), ...deps };
|
|
653
682
|
const active = new Map();
|
|
654
683
|
const waiting = [];
|
|
@@ -668,10 +697,13 @@ function collectSnapshot({ root = process.cwd(), deps = defaultDeps() } = {}) {
|
|
|
668
697
|
if (owner) active.set(sanitizeAgent(owner), clip(worktreeTask(item.root, allDeps) || 'worktree changes', 90));
|
|
669
698
|
}
|
|
670
699
|
let landingWait = 0;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
700
|
+
if (!skipLanding) {
|
|
701
|
+
try {
|
|
702
|
+
// Same options as the landing-state event so both share one board.
|
|
703
|
+
const board = sharedBoard(root);
|
|
704
|
+
landingWait = Number(board?.summary?.due || 0);
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
675
707
|
const activeRows = [...active.entries()];
|
|
676
708
|
const lines = [];
|
|
677
709
|
lines.push(`Team stream: ${activeRows.length} active agent${activeRows.length === 1 ? '' : 's'}.`);
|
|
@@ -838,6 +870,8 @@ function streamCommand(args = [], deps = defaultDeps()) {
|
|
|
838
870
|
if (closed || polling) return;
|
|
839
871
|
polling = true;
|
|
840
872
|
try {
|
|
873
|
+
clearBoardMemo();
|
|
874
|
+
clearWorktreeConfigMemo(allDeps);
|
|
841
875
|
const fresh = pollStreamOnce(state, { root, sinceMs: opts.sinceMs || (nowMs - 24 * 60 * 60 * 1000), agent: opts.agent, deps: allDeps, nowMs: allDeps.now() });
|
|
842
876
|
if (fresh.length) console.log(renderRecords(fresh, opts));
|
|
843
877
|
} finally {
|