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.
Files changed (41) hide show
  1. package/README.md +37 -5
  2. package/atris/skills/engines/SKILL.md +9 -9
  3. package/bin/atris.js +1 -0
  4. package/commands/autoland.js +36 -5
  5. package/commands/autopilot.js +105 -6
  6. package/commands/engine.js +96 -21
  7. package/commands/host.js +56 -4
  8. package/commands/land.js +129 -8
  9. package/commands/member.js +33 -8
  10. package/commands/mission.js +112 -3
  11. package/commands/one-lap.js +14 -10
  12. package/commands/stream.js +56 -22
  13. package/commands/team.js +82 -10
  14. package/commands/worktree.js +39 -0
  15. package/commands/youtube.js +36 -26
  16. package/lib/engine-ask.js +112 -9
  17. package/lib/engine-registry.js +283 -39
  18. package/lib/fleet.js +444 -26
  19. package/lib/functional-owner.js +15 -6
  20. package/lib/host.js +311 -175
  21. package/lib/member-engine.js +11 -1
  22. package/lib/member-park.js +138 -0
  23. package/lib/member-scaffold.js +37 -0
  24. package/lib/roster-markdown.js +1 -1
  25. package/lib/roster-prep.js +289 -0
  26. package/lib/roster-runs.js +389 -0
  27. package/lib/roster-suggest.js +145 -0
  28. package/lib/router-brain.js +111 -3
  29. package/lib/self-drive.js +26 -2
  30. package/lib/state-detection.js +3 -3
  31. package/lib/task-db.js +38 -3
  32. package/lib/team-lineup.js +7 -2
  33. package/lib/todo.js +1 -1
  34. package/lib/wish-audit.js +1 -1
  35. package/package.json +1 -1
  36. package/scripts/det/cpu-hotspots.js +216 -0
  37. package/scripts/det/ytnotes +105 -19
  38. package/scripts/det/ytquote-repair.js +18 -4
  39. package/templates/members/host/MEMBER.md +5 -3
  40. package/templates/members/host/MISSION.md +1 -1
  41. package/templates/members/host/skills/host/SKILL.md +61 -12
@@ -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
- console.log(`${members.length} ${members.length === 1 ? 'member' : 'members'} found.`);
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> Install the full MEMBER bundle if missing; never overwrite');
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');
@@ -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
- const status = result && result.status === 'ran' ? 'ready' : engineFailureHealthStatus(result);
451
- return status ? setEngineHealth(engineId, status, root) : null;
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,
@@ -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.health || selected.health.status !== 'ready') throw new Error(`engine ${selected.id} is not ready`);
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.health && engine.health.status === 'ready')
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.health && engine.health.status === 'ready');
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 ['roster_model', 'roster_effort', 'roster_max_seconds']) {
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, and
217
- // time cap for the builder, and each reviewer's for that reviewer only. A
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.health && entry.health.status === 'ready')
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,
@@ -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 { collectBoard } = require('./land');
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: (root, args) => spawnGit(args, { cwd: root, check: false }),
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
- const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-owner$'], deps);
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
- const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-task$'], deps);
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 = collectBoard(root);
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
- function collectSnapshot({ root = process.cwd(), deps = defaultDeps() } = {}) {
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
- try {
672
- const board = collectBoard(root);
673
- landingWait = Number(board?.summary?.due || 0);
674
- } catch {}
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 {