atris 3.70.0 → 3.71.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 (76) hide show
  1. package/README.md +18 -0
  2. package/atris/skills/README.md +1 -0
  3. package/atris/skills/campaigns/SKILL.md +81 -0
  4. package/atris/skills/meta/SKILL.md +52 -8
  5. package/ax +226 -42
  6. package/bin/atris.js +26 -6
  7. package/commands/autoland.js +176 -7
  8. package/commands/autopilot.js +71 -11
  9. package/commands/campaign.js +759 -0
  10. package/commands/compile.js +19 -8
  11. package/commands/doctor.js +58 -0
  12. package/commands/dream.js +18 -3
  13. package/commands/engine.js +147 -5
  14. package/commands/improve.js +119 -9
  15. package/commands/init.js +18 -1
  16. package/commands/interview.js +15 -3
  17. package/commands/land.js +22 -6
  18. package/commands/link.js +1 -0
  19. package/commands/member.js +359 -94
  20. package/commands/mission.js +249 -36
  21. package/commands/report.js +42 -8
  22. package/commands/run.js +9 -2
  23. package/commands/scout.js +9 -1
  24. package/commands/task.js +71 -17
  25. package/commands/team.js +212 -4
  26. package/commands/unknowns.js +15 -1
  27. package/commands/verify.js +46 -0
  28. package/commands/worktree.js +108 -15
  29. package/commands/write.js +12 -1
  30. package/commands/youtube.js +17 -2
  31. package/lib/accept-verify-gate.js +1 -0
  32. package/lib/auto-accept-certified.js +1 -0
  33. package/lib/autoland.js +13 -4
  34. package/lib/campaign-cadence.js +131 -0
  35. package/lib/campaign-core.js +330 -0
  36. package/lib/campaign-holdout.js +65 -0
  37. package/lib/campaign-loop.js +242 -0
  38. package/lib/campaign-missions.js +237 -0
  39. package/lib/campaign-new.js +251 -0
  40. package/lib/campaign-protect.js +75 -0
  41. package/lib/campaign-reading.js +147 -0
  42. package/lib/campaign-schema.js +196 -0
  43. package/lib/campaign-store.js +380 -0
  44. package/lib/campaign-sync.js +302 -0
  45. package/lib/campaign-trace.js +300 -0
  46. package/lib/conductor-artifacts.js +15 -1
  47. package/lib/daily-log.js +11 -1
  48. package/lib/dispatch-scout.js +18 -0
  49. package/lib/engine-ask.js +33 -3
  50. package/lib/engine-registry.js +2 -0
  51. package/lib/engine-resource.js +518 -0
  52. package/lib/fleet.js +125 -16
  53. package/lib/init-commit.js +80 -0
  54. package/lib/intents.js +8 -0
  55. package/lib/known-commands.js +1 -1
  56. package/lib/land-green.js +58 -7
  57. package/lib/landing-proof.js +352 -0
  58. package/lib/member-autoland.js +395 -18
  59. package/lib/member-board.js +7 -1
  60. package/lib/member-map-fold.js +126 -0
  61. package/lib/member-map-inbox.js +80 -0
  62. package/lib/member-progress.js +48 -1
  63. package/lib/member-report.js +127 -8
  64. package/lib/member-self-next-job.js +97 -0
  65. package/lib/member-task-pick.js +41 -14
  66. package/lib/member-up.js +249 -16
  67. package/lib/roster-prep.js +25 -2
  68. package/lib/runner-command.js +5 -0
  69. package/lib/task-db.js +47 -11
  70. package/lib/task-explanation.js +7 -0
  71. package/lib/task-list-keeper.js +8 -1
  72. package/lib/task-receipt.js +6 -1
  73. package/mcp/atris-mcp/index.mjs +212 -10
  74. package/package.json +2 -1
  75. package/scripts/det/engine-reading.js +37 -0
  76. package/scripts/det/ytnotes +29 -5
@@ -566,7 +566,16 @@ function memberLatestMission(name, missionMap = memberRunMissionMap()) {
566
566
 
567
567
  function memberLiveMission(name, missionMap = memberRunMissionMap()) {
568
568
  return memberOwnedMissions(name, missionMap)
569
- .find((mission) => MEMBER_LIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) || null;
569
+ .find((mission) => {
570
+ if (!MEMBER_LIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) return false;
571
+ // A closed window can leave a running row behind. Only a known dead
572
+ // process proves it is free; missing process records still hold it.
573
+ try {
574
+ const root = mission.worktree_root || mission.worktree?.path || process.cwd();
575
+ const health = require('./mission').missionDriverHealth(mission, root);
576
+ return !health.pid || health.alive;
577
+ } catch { return true; }
578
+ }) || null;
570
579
  }
571
580
 
572
581
  function missionShortName(mission, max = 36) {
@@ -922,7 +931,10 @@ function memberRunDefaultWallSeconds(name, cwd = process.cwd()) {
922
931
  const picked = require('../lib/member-engine').memberRosterEngine(name, cwd);
923
932
  cap = Number(picked && picked.engine && picked.engine.roster_max_seconds) || 0;
924
933
  } catch {}
925
- return Math.max(900, cap ? cap + 600 : 0);
934
+ // No roster cap means the tick runs on the default tick cap, so the run
935
+ // needs that much plus review time, or the wall kills it first.
936
+ const tickSeconds = cap || require('./mission').missionTickTimeoutMs({}, Infinity) / 1000;
937
+ return Math.max(900, tickSeconds + 600);
926
938
  }
927
939
 
928
940
  function pushFlagValue(out, args, name) {
@@ -1254,7 +1266,7 @@ function reviewRefusalLine(refusal) {
1254
1266
  return line.trim().slice(0, 200);
1255
1267
  }
1256
1268
 
1257
- function memberRunWith(name, args, { quiet = false } = {}) {
1269
+ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
1258
1270
  if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
1259
1271
  console.log('usage: atris member run <name> ["mission text"] [--verify "<cmd>"] [--minutes N|--hours N] [--json]');
1260
1272
  console.log('pick for me: atris member run "fix the flaky login test" (the best member takes it and runs it)');
@@ -1333,6 +1345,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1333
1345
  console.log(`watch: tail -f ${progressDir}/${String(name).replace(/[^A-Za-z0-9._-]/g, '_')}.log`);
1334
1346
  console.log('board: atris member board --watch');
1335
1347
  console.log('stop: atris member stop <name>');
1348
+ console.log(`where work goes: ${require('../lib/member-autoland').landingDestination(mission.worktree?.path || process.cwd())}`);
1336
1349
  }
1337
1350
 
1338
1351
  const cwd = mission.worktree?.path || process.cwd();
@@ -1406,7 +1419,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1406
1419
  const summary = ticks.map((tick) => tick?.summary || tick?.claude?.summary).filter(Boolean).pop() || '';
1407
1420
  const workerEngine = ticks.map((tick) => tick?.engine_id).filter(Boolean).pop()
1408
1421
  || runnerSwap?.to || readFlag(args, '--runner', '') || '';
1409
- result.autoland = require('../lib/member-autoland').autolandMemberRun({ mission: finalMission, cwd, summary, workerEngine });
1422
+ result.autoland = require('../lib/member-autoland').autolandMemberRun({ mission: finalMission, cwd, summary, workerEngine, closeTaskIds });
1410
1423
  if (!asJson && !quiet) console.log(result.autoland.line);
1411
1424
  }
1412
1425
  if (checkFailed) {
@@ -3343,6 +3356,19 @@ function dateFromLogPath(relativePath) {
3343
3356
  return match ? match[1] : null;
3344
3357
  }
3345
3358
 
3359
+ // Mission driver logs, goals files, and the wiki log have no date in their
3360
+ // name; without one they skipped the 7-day window and months-old lines kept
3361
+ // counting as this week's pain. The last write date stands in for them.
3362
+ function logDateForScan(relativePath, filePath) {
3363
+ const named = dateFromLogPath(relativePath);
3364
+ if (named) return named;
3365
+ try {
3366
+ return fs.statSync(filePath).mtime.toISOString().slice(0, 10);
3367
+ } catch {
3368
+ return null;
3369
+ }
3370
+ }
3371
+
3346
3372
  function compareLogEvidenceRecentFirst(a, b) {
3347
3373
  const dateA = a?.date || '';
3348
3374
  const dateB = b?.date || '';
@@ -3407,7 +3433,11 @@ function isAutoImproverGeneratedLogLine(line, relativePath = '') {
3407
3433
  if (/^[-*]?\s*Next tick will verify failed, halting\.?$/i.test(text)) return true;
3408
3434
  if (/^##\s+\d{1,2}:\d{2}\s+·\s+Task failed$/i.test(text)) return true;
3409
3435
  if (/^[-*]?\s*(status|action|state|verifier)\s*:\s*(failed|blocked)\s*$/i.test(text)) return true;
3410
- if (/^[-*]?\s*(title|proof):\s*/i.test(text)) return true;
3436
+ // Mission lines state a goal; their words ("leftover error text", "done failed") are not failures.
3437
+ // Goal files say the same in JSON ("stop_rule": "Stop if ... risk is unclear").
3438
+ if (/^[-*]?\s*"?(title|proof|proof_target|mission|objective|acceptance|stop_rule)"?\s*:\s*/i.test(text)) return true;
3439
+ // Driver logs print "error": null on every clean run.
3440
+ if (/^"?(error|failure)s?"?\s*:\s*(null|false|""|\[\]|\{\})\s*,?$/i.test(text)) return true;
3411
3441
  if (String(relativePath || '').startsWith('atris/team/auto-improver/logs/') && /^-\s*summary:\s*/i.test(text)) return true;
3412
3442
  if (/\bauto[- ]improver\b/i.test(text) && /\b(dogfood|receipt|prevented|pain_)\b/i.test(text)) return true;
3413
3443
  if (/\bauto_improver\b/i.test(text)) return true;
@@ -3425,7 +3455,9 @@ function normalizeFailurePattern(line) {
3425
3455
  .trim());
3426
3456
  }
3427
3457
 
3428
- function failureCoveredByPassLesson(line, passLessonText = '') {
3458
+ // Callers scanning many lines pass passLessonLower so the lessons file is
3459
+ // lowercased once per scan, not once per log line.
3460
+ function failureCoveredByPassLesson(line, passLessonText = '', passLessonLower = passLessonText.toLowerCase()) {
3429
3461
  const text = String(line || '');
3430
3462
  if (!text || !passLessonText) return false;
3431
3463
  if (/spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(text)) {
@@ -3441,23 +3473,59 @@ function failureCoveredByPassLesson(line, passLessonText = '') {
3441
3473
  const bare = pattern.replace(/^#+\s*[#:.]*\s*·\s*/, '').trim();
3442
3474
  for (const candidate of [pattern, bare]) {
3443
3475
  if (candidate && candidate.length >= 12
3444
- && passLessonText.toLowerCase().includes(candidate.toLowerCase())) {
3476
+ && passLessonLower.includes(candidate.toLowerCase())) {
3445
3477
  return true;
3446
3478
  }
3447
3479
  }
3448
3480
  return false;
3449
3481
  }
3450
3482
 
3483
+ // Runtime logs the repeat-failure scan reads, listed by where they live
3484
+ // instead of one capped walk of atris/team. That folder holds hundreds of
3485
+ // skill and mission files, and a 220-file walk ran out before it reached
3486
+ // most members' logs, so this week's repeats counted as zero. Listing paths
3487
+ // is cheap; the 7-day filter below decides what actually gets read.
3488
+ function listAutoImproverRuntimeLogs(root) {
3489
+ const listAll = (dir) => (fs.existsSync(dir) ? listFilesBounded(dir, { maxFiles: 5000 }) : []);
3490
+ const childDirs = (dir) => {
3491
+ try {
3492
+ return fs.readdirSync(dir, { withFileTypes: true })
3493
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
3494
+ .map((entry) => entry.name)
3495
+ .sort();
3496
+ } catch {
3497
+ return [];
3498
+ }
3499
+ };
3500
+ const files = [];
3501
+ const logsRoot = path.join(root, 'atris', 'logs');
3502
+ try {
3503
+ for (const entry of fs.readdirSync(logsRoot, { withFileTypes: true })) {
3504
+ if (entry.isFile()) files.push(path.join(logsRoot, entry.name));
3505
+ }
3506
+ } catch {}
3507
+ for (const name of childDirs(logsRoot)) {
3508
+ if (name !== 'archive') files.push(...listAll(path.join(logsRoot, name)));
3509
+ }
3510
+ const teamRoot = path.join(root, 'atris', 'team');
3511
+ for (const member of childDirs(teamRoot)) {
3512
+ for (const name of ['goals.md', 'goals.json']) {
3513
+ const goalsPath = path.join(teamRoot, member, name);
3514
+ if (fs.existsSync(goalsPath)) files.push(goalsPath);
3515
+ }
3516
+ files.push(...listAll(path.join(teamRoot, member, 'logs')));
3517
+ }
3518
+ const wikiLog = path.join(root, 'atris', 'wiki', 'log.md');
3519
+ if (fs.existsSync(wikiLog)) files.push(wikiLog);
3520
+ return files;
3521
+ }
3522
+
3451
3523
  function collectAutoImproverLogSignals(root) {
3452
- const roots = [
3453
- path.join(root, 'atris', 'logs'),
3454
- path.join(root, 'atris', 'team'),
3455
- path.join(root, 'atris', 'wiki'),
3456
- ].filter((candidate) => fs.existsSync(candidate));
3457
3524
  const passLessonText = safeReadText(path.join(root, 'atris', 'lessons.md'), 500000)
3458
3525
  .split(/\r?\n/)
3459
3526
  .filter((line) => /\s+pass\s*[-\u2014:,]/.test(line))
3460
3527
  .join('\n');
3528
+ const passLessonLower = passLessonText.toLowerCase();
3461
3529
  const failureRegex = /\b(error|failed|failure|blocked|timeout|regression|crash|missing proof|naraka|suffering)\b/i;
3462
3530
  const unclearRegex = /\b(tbd|unclear|unknown|needs user|needs owner|needs proof|no next|blocked)\b/i;
3463
3531
  const counts = new Map();
@@ -3465,66 +3533,67 @@ function collectAutoImproverLogSignals(root) {
3465
3533
  let filesScanned = 0;
3466
3534
  let linesScanned = 0;
3467
3535
  let unclearNextActions = 0;
3468
- for (const scanRoot of roots) {
3469
- for (const filePath of listFilesBounded(scanRoot)) {
3470
- const relative = repoRelative(root, filePath);
3471
- const logDate = dateFromLogPath(relative);
3472
- if (relative.startsWith('atris/logs/archive/')) continue;
3473
- const isRuntimeLog = relative.startsWith('atris/logs/')
3474
- || /^atris\/team\/[^/]+\/logs\//.test(relative)
3475
- || /^atris\/team\/[^/]+\/goals\.(md|json)$/.test(relative)
3476
- || relative === 'atris/wiki/log.md';
3477
- if (!isRuntimeLog) continue;
3478
- // Only scan logs from the last 7 days. Historical log lines keep
3479
- // triggering false-positive findings long after their root cause is
3480
- // fixed (e.g. "goal-from-mission blocked" from July when MISSION.md
3481
- // was repaired in August). Old evidence stays on disk for audits;
3482
- // it just stops feeding the pain score, so pain can actually drop.
3483
- if (logDate && isRuntimeLog) {
3484
- const logAgeDays = (Date.now() - new Date(logDate + 'T12:00:00').getTime()) / 864e5;
3485
- if (logAgeDays > 7) continue;
3486
- }
3487
- const text = safeReadText(filePath);
3488
- if (!text) continue;
3489
- filesScanned += 1;
3490
- const allLines = text.split(/\r?\n/);
3491
- const lineOffset = Math.max(0, allLines.length - 500);
3492
- const lines = allLines.slice(-500);
3493
- linesScanned += lines.length;
3494
- for (let index = 0; index < lines.length; index += 1) {
3495
- const sourceLine = lineOffset + index + 1;
3496
- const line = lines[index];
3497
- if (isAutoImproverGeneratedLogLine(line, relative)) continue;
3498
- // Declared verification receipts ("check: <command> ...") describe what
3499
- // was verified, not what broke. Wiki upkeep sweeps write one per page,
3500
- // so counting them as failures spawns bogus recurring-pattern tasks
3501
- // (CLI-199 came from 13 such lines in atris/wiki/log.md).
3502
- if (/^\s*[-*]?\s*check:\s/i.test(line)) continue;
3503
- if (/\b(errors?|fail(?:ed|ures?)|blocked|timeouts?)\s*:\s*0\b/i.test(line)) continue;
3504
- if (/\bblocked\s*(?:->|→|to)\s*ready\b/i.test(line)) continue;
3505
- if (failureCoveredByPassLesson(line, passLessonText)) continue;
3506
- if (unclearRegex.test(line)) {
3507
- unclearNextActions += 1;
3508
- unclearActions.push({
3509
- path: relative,
3510
- date: logDate,
3511
- line: sourceLine,
3512
- text: compactSentence(line, 180),
3513
- });
3514
- }
3515
- if (!failureRegex.test(line)) continue;
3516
- const pattern = normalizeFailurePattern(line);
3517
- if (!pattern) continue;
3518
- const existing = counts.get(pattern) || { pattern, count: 0, evidence: [] };
3519
- existing.count += 1;
3520
- existing.evidence.push({
3536
+ for (const filePath of listAutoImproverRuntimeLogs(root)) {
3537
+ const relative = repoRelative(root, filePath);
3538
+ const logDate = logDateForScan(relative, filePath);
3539
+ if (relative.startsWith('atris/logs/archive/')) continue;
3540
+ const isRuntimeLog = relative.startsWith('atris/logs/')
3541
+ || /^atris\/team\/[^/]+\/logs\//.test(relative)
3542
+ || /^atris\/team\/[^/]+\/goals\.(md|json)$/.test(relative)
3543
+ || relative === 'atris/wiki/log.md';
3544
+ if (!isRuntimeLog) continue;
3545
+ // Only scan logs from the last 7 days. Historical log lines keep
3546
+ // triggering false-positive findings long after their root cause is
3547
+ // fixed (e.g. "goal-from-mission blocked" from July when MISSION.md
3548
+ // was repaired in August). Old evidence stays on disk for audits;
3549
+ // it just stops feeding the pain score, so pain can actually drop.
3550
+ if (logDate && isRuntimeLog) {
3551
+ const logAgeDays = (Date.now() - new Date(logDate + 'T12:00:00').getTime()) / 864e5;
3552
+ if (logAgeDays > 7) continue;
3553
+ }
3554
+ const text = safeReadText(filePath);
3555
+ if (!text) continue;
3556
+ filesScanned += 1;
3557
+ const allLines = text.split(/\r?\n/);
3558
+ const lineOffset = Math.max(0, allLines.length - 500);
3559
+ const lines = allLines.slice(-500);
3560
+ linesScanned += lines.length;
3561
+ for (let index = 0; index < lines.length; index += 1) {
3562
+ const sourceLine = lineOffset + index + 1;
3563
+ const line = lines[index];
3564
+ if (isAutoImproverGeneratedLogLine(line, relative)) continue;
3565
+ // Declared verification receipts ("check: <command> ...") describe what
3566
+ // was verified, not what broke. Wiki upkeep sweeps write one per page,
3567
+ // so counting them as failures spawns bogus recurring-pattern tasks
3568
+ // (CLI-199 came from 13 such lines in atris/wiki/log.md).
3569
+ if (/^\s*[-*]?\s*check:\s/i.test(line)) continue;
3570
+ if (/\b(errors?|fail(?:ed|ures?)|blocked|timeouts?)\s*:\s*0\b/i.test(line)) continue;
3571
+ if (/\bblocked\s*(?:->|→|to)\s*ready\b/i.test(line)) continue;
3572
+ if (failureCoveredByPassLesson(line, passLessonText, passLessonLower)) continue;
3573
+ // A file name is not a next step ("receipt: atris/runs/...-unknown-word-...json"),
3574
+ // and "learned:" / "why:" lines explain the past, so neither counts as unclear.
3575
+ const unclearText = line.replace(/[\w.@~-]*\/[\w./@~-]*\.\w{1,5}\b/g, ' ');
3576
+ if (!/^\s*[-*]?\s*(learned|why)\s*:/i.test(line) && unclearRegex.test(unclearText)) {
3577
+ unclearNextActions += 1;
3578
+ unclearActions.push({
3521
3579
  path: relative,
3522
3580
  date: logDate,
3523
3581
  line: sourceLine,
3524
3582
  text: compactSentence(line, 180),
3525
3583
  });
3526
- counts.set(pattern, existing);
3527
3584
  }
3585
+ if (!failureRegex.test(line)) continue;
3586
+ const pattern = normalizeFailurePattern(line);
3587
+ if (!pattern) continue;
3588
+ const existing = counts.get(pattern) || { pattern, count: 0, evidence: [] };
3589
+ existing.count += 1;
3590
+ existing.evidence.push({
3591
+ path: relative,
3592
+ date: logDate,
3593
+ line: sourceLine,
3594
+ text: compactSentence(line, 180),
3595
+ });
3596
+ counts.set(pattern, existing);
3528
3597
  }
3529
3598
  }
3530
3599
  const repeated = [...counts.values()]
@@ -9008,6 +9077,26 @@ async function memberLoopWakeTick(name, run, index, tickStartedAt) {
9008
9077
  const MEMBER_LOOP_IDLE_BREAK_THRESHOLD = 2;
9009
9078
  const MEMBER_LOOP_IDENTICAL_ASK_BREAK_THRESHOLD = 2;
9010
9079
 
9080
+ // A loop kept running for days would keep old code after an atris upgrade. Read the
9081
+ // installed version from disk, not the cached require, so a new install is seen.
9082
+ function memberLoopInstalledVersion() {
9083
+ try {
9084
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version || null;
9085
+ } catch {
9086
+ return null;
9087
+ }
9088
+ }
9089
+
9090
+ // Start the same loop again on the new code. The lease is already released, so the
9091
+ // fresh process takes it over; this one exits with the fresh run's status.
9092
+ function memberLoopRestartOnNewVersion(upgrade) {
9093
+ const child = spawnSync(process.execPath, process.argv.slice(1), {
9094
+ stdio: 'inherit',
9095
+ env: { ...process.env, ATRIS_MEMBER_LOOP_RESTARTED_FROM: upgrade.from },
9096
+ });
9097
+ process.exit(child.status == null ? 1 : child.status);
9098
+ }
9099
+
9011
9100
  // Loop phase: control. Hold the lease, honor the stop file, pace ticks by interval, and
9012
9101
  // break early when the member is idling instead of faking activity.
9013
9102
  async function memberLoopRunTicks(name, paths, lease, run) {
@@ -9018,6 +9107,13 @@ async function memberLoopRunTicks(name, paths, lease, run) {
9018
9107
  state.stopped = true;
9019
9108
  break;
9020
9109
  }
9110
+ if (index > 0 && plan.forever && state.startVersion) {
9111
+ const installed = memberLoopInstalledVersion();
9112
+ if (installed && installed !== state.startVersion) {
9113
+ state.upgrade = { from: state.startVersion, to: installed };
9114
+ break;
9115
+ }
9116
+ }
9021
9117
  refreshMemberLoopLease(paths, lease.lease, plan.ttlMs);
9022
9118
  const tickStartedAt = stampIso();
9023
9119
  try {
@@ -9071,7 +9167,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
9071
9167
  const { options, plan, state } = run;
9072
9168
  const { execute, aliveMode, asJson } = options;
9073
9169
  const { hourly, forever, runId, ticks, intervalMs, durationMs, startedAt } = plan;
9074
- const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath } = state;
9170
+ const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath, upgrade } = state;
9075
9171
  const finishedAt = stampIso();
9076
9172
  const plannedOnly = aliveMode && tickResults.length > 0 && tickResults.every((tick) => tick.decision === 'planned');
9077
9173
  const summary = {
@@ -9080,7 +9176,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
9080
9176
  schema: aliveMode ? 'atris.member_alive.v1' : 'atris.member_loop.v1',
9081
9177
  member: name,
9082
9178
  alive: aliveMode,
9083
- status: failed ? 'failed' : stopped ? 'stopped' : plannedOnly ? 'planned' : earlyExit ? (earlyExit.blocked_on_human ? 'blocked_on_human' : 'idle') : 'completed',
9179
+ status: failed ? 'failed' : stopped ? 'stopped' : upgrade ? 'restarting_on_new_version' : plannedOnly ? 'planned' : earlyExit ? (earlyExit.blocked_on_human ? 'blocked_on_human' : 'idle') : 'completed',
9084
9180
  mode: execute ? 'execute' : 'dry_run',
9085
9181
  cadence: hourly ? 'hourly' : 'custom',
9086
9182
  forever,
@@ -9096,6 +9192,8 @@ function memberLoopRecordResult(name, paths, lease, run) {
9096
9192
  duration_ms_actual: Date.parse(finishedAt) - Date.parse(startedAt),
9097
9193
  decisions,
9098
9194
  early_exit: earlyExit,
9195
+ upgrade: upgrade || null,
9196
+ restarted_from_version: process.env.ATRIS_MEMBER_LOOP_RESTARTED_FROM || null,
9099
9197
  stop: earlyExit?.stop || null,
9100
9198
  blocked_on_human: earlyExit?.blocked_on_human === true,
9101
9199
  needs_user: earlyExit?.needs_user === true,
@@ -9120,6 +9218,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
9120
9218
  `Status: ${payload.status}`,
9121
9219
  `Cadence: ${payload.cadence}${payload.forever ? ' forever' : ''}`,
9122
9220
  `Ticks: ${payload.ticks}/${payload.ticks_requested}`,
9221
+ ...(upgrade ? [`Upgrade: atris ${upgrade.from} -> ${upgrade.to}, restarting on the new version`] : []),
9123
9222
  `Decisions: ${Object.entries(decisions).map(([key, count]) => `${key} x${count}`).join(', ') || 'none'}`,
9124
9223
  `Stop: ${payload.stop_command}`,
9125
9224
  `Receipt: ${path.relative(process.cwd(), receiptPath)}`,
@@ -9185,11 +9284,14 @@ async function memberLoop(name, ...args) {
9185
9284
  lastAskKey: null,
9186
9285
  consecutiveIdenticalAsk: 0,
9187
9286
  tickLogPath,
9287
+ startVersion: memberLoopInstalledVersion(),
9288
+ upgrade: null,
9188
9289
  };
9189
9290
 
9190
9291
  const run = { options, plan, state };
9191
9292
  await memberLoopRunTicks(name, paths, lease, run);
9192
9293
  memberLoopRecordResult(name, paths, lease, run);
9294
+ if (state.upgrade) memberLoopRestartOnNewVersion(state.upgrade);
9193
9295
  }
9194
9296
 
9195
9297
  async function memberTick(name, ...args) {
@@ -9856,10 +9958,26 @@ const EXPERIMENT_STATUSES = new Set(['experimental', 'one-off']);
9856
9958
 
9857
9959
  function ideaRuleFor(slug, record, text) {
9858
9960
  const own = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(slug));
9859
- if (own && own.pattern.test(text)) return own;
9961
+ if (own && own.pattern.test(text)) return { rule: own, own: true };
9860
9962
  const { autoJobForMember } = require('../lib/member-engine');
9861
9963
  const archetype = ARCHETYPE_FOR_JOB[autoJobForMember(slug, record.role)];
9862
- return IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(archetype)) || null;
9964
+ const rule = IDEA_ARCHETYPE_RULES.find((r) => r.slugs.includes(archetype)) || null;
9965
+ return { rule, own: Boolean(own && rule === own) };
9966
+ }
9967
+
9968
+ // The request's job is the first rule it matches; the list runs from
9969
+ // specific jobs to the general build job, so "where is the code" is a
9970
+ // navigator job even though "code" is also a build word.
9971
+ function ideaJobRule(text) {
9972
+ return IDEA_ARCHETYPE_RULES.find((rule) => rule.pattern.test(text)) || null;
9973
+ }
9974
+
9975
+ // 3 = named in the idea, 2 = the job is the member's own, 1 = the member
9976
+ // falls back to the job, 0 = not its job. Fit outranks shared words.
9977
+ function ideaJobFit(named, memberRule, jobRule) {
9978
+ if (named) return 3;
9979
+ if (!jobRule || !memberRule.rule || memberRule.rule !== jobRule) return 0;
9980
+ return memberRule.own ? 2 : 1;
9863
9981
  }
9864
9982
 
9865
9983
  // 0 = normal, 1 = experiment or one-off, 2 = parked, human, or odd-typed.
@@ -9871,10 +9989,12 @@ function dispatchTier(record) {
9871
9989
  return 0;
9872
9990
  }
9873
9991
 
9874
- // Lower tier wins first, then higher score, then name order for a stable pick.
9992
+ // Lower tier wins first, then job fit, then higher score, then name order
9993
+ // for a stable pick.
9875
9994
  function outranksDispatch(candidate, best) {
9876
9995
  if (!best) return true;
9877
9996
  if (candidate.tier !== best.tier) return candidate.tier < best.tier;
9997
+ if (candidate.fit !== best.fit) return candidate.fit > best.fit;
9878
9998
  if (candidate.score !== best.score) return candidate.score > best.score;
9879
9999
  return candidate.member.localeCompare(best.member) < 0;
9880
10000
  }
@@ -9908,6 +10028,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9908
10028
  .toLowerCase()
9909
10029
  .split(/[^a-z0-9]+/)
9910
10030
  .filter((w) => w.length >= 4);
10031
+ const jobRule = ideaJobRule(text);
9911
10032
 
9912
10033
  let best = null;
9913
10034
  for (const record of members) {
@@ -9919,10 +10040,11 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9919
10040
  let score = 0;
9920
10041
  let matchedReason = '';
9921
10042
 
9922
- const rule = ideaRuleFor(slug, record, text);
9923
- if (rule && rule.pattern.test(text)) {
10043
+ const memberRule = ideaRuleFor(slug, record, text);
10044
+ const fit = ideaJobFit(named, memberRule, jobRule);
10045
+ if (fit > 0 && memberRule.rule) {
9924
10046
  score += 30;
9925
- matchedReason = rule.reason;
10047
+ matchedReason = memberRule.rule.reason;
9926
10048
  }
9927
10049
 
9928
10050
  const missionFile = path.join(teamDir, slug, 'MISSION.md');
@@ -9953,6 +10075,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9953
10075
  reason: matchedReason,
9954
10076
  score,
9955
10077
  tier,
10078
+ fit,
9956
10079
  };
9957
10080
  if (outranksDispatch(candidate, best)) best = candidate;
9958
10081
  }
@@ -10445,6 +10568,29 @@ function isKnownMemberSlug(token, root = process.cwd()) {
10445
10568
  || fs.existsSync(path.join(root, 'atris', 'team', `${clean}.md`));
10446
10569
  }
10447
10570
 
10571
+ const MEMBER_SUBCOMMANDS = [
10572
+ 'list', 'roster', 'create', 'install', 'activate', 'upgrade', 'push', 'pull', 'goal',
10573
+ 'chat', 'tick', 'wake', 'run', 'result', 'go', 'work', 'up', 'steer', 'sleep', 'rest',
10574
+ 'room', 'day', 'standup', 'handoff', 'dispatch', 'feed', 'board', 'stop', 'card', 'ping',
10575
+ 'loop', 'alive', 'review', 'block', 'status', 'live', 'heartbeat', 'improvements',
10576
+ 'report', 'history', 'supervisor', 'export', 'archive', 'tidy', 'help',
10577
+ ];
10578
+
10579
+ // `atris member <word>` where the word is neither a member nor a subcommand.
10580
+ // Points at the nearest name and the team, and exits 1 without running anyone.
10581
+ function memberUnknownWord(word, root = process.cwd()) {
10582
+ const { suggestCommand } = require('../lib/known-commands');
10583
+ let names = [];
10584
+ try { names = findAllMembers(path.join(root, 'atris', 'team')).map((m) => m.name); } catch {}
10585
+ const near = suggestCommand(word, [...MEMBER_SUBCOMMANDS, ...names]);
10586
+ console.log(`No member or command named "${word}", so nothing started.`);
10587
+ if (near) console.log(`Did you mean: atris member ${near}`);
10588
+ if (names.length) console.log(`Your team: ${names.join(', ')}`);
10589
+ console.log('To give a member a job: atris member run "<job in a few words>"');
10590
+ process.exitCode = 1;
10591
+ return { ok: false, reason: 'unknown_word', word, suggestion: near || null };
10592
+ }
10593
+
10448
10594
  // The job a member does (build, review or search), read from the same team
10449
10595
  // roster that picks its engine.
10450
10596
  function memberGoJob(owner, root) {
@@ -10468,10 +10614,10 @@ function memberGoTask(owner, root, { claim, requireCheck = false }) {
10468
10614
  const job = memberGoJob(owner, root);
10469
10615
  const workspaceRoot = taskDb.workspaceRoot(root);
10470
10616
  if (claim) {
10471
- const got = picker.claimTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck });
10617
+ const got = picker.claimTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck, recoverClaimed: true });
10472
10618
  return got ? { ...got, job } : null;
10473
10619
  }
10474
- const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, job, requireCheck });
10620
+ const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck, recoverClaimed: true });
10475
10621
  return task ? { task, objective: picker.taskObjective(task), job } : null;
10476
10622
  } catch (error) {
10477
10623
  console.error(`could not take a task from the list: ${String(error && error.message || error).split('\n')[0]}`);
@@ -10493,8 +10639,12 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
10493
10639
  if (!thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked) {
10494
10640
  const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
10495
10641
  receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
10496
- const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof });
10642
+ // The commit comes from the member's own work copy, not the main folder.
10643
+ const workRoot = (result.worktree && result.worktree.path) || (mission.worktree && mission.worktree.path) || undefined;
10644
+ const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof, workRoot });
10497
10645
  if (ready.ready !== false) return { id: task.id, title: task.title, outcome: 'review', receipt: receipt || null };
10646
+ // Its change already merged and the merge line closed the job.
10647
+ if (taskDb.getTask(db, task.id)?.status === 'done') return { id: task.id, title: task.title, outcome: 'done', receipt: receipt || null };
10498
10648
  }
10499
10649
  const ticks = Array.isArray(result && result.ticks) ? result.ticks : [];
10500
10650
  const lastReason = String((ticks[ticks.length - 1] || {}).reason || (ticks[ticks.length - 1] || {}).error || '');
@@ -10504,10 +10654,19 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
10504
10654
  : (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
10505
10655
  const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
10506
10656
  try { taskDb.noteTask(db, { id: task.id, actor: owner, content: `Released by ${owner}: ${reason}` }); } catch {}
10657
+ // Work staged on a protected surface waits for a person to review it; a
10658
+ // member picking the task again would only redo it and stop again.
10659
+ let held = false;
10660
+ const forReview = Boolean(!thrown && result && result.needs_review === true);
10661
+ if (forReview) {
10662
+ try {
10663
+ held = taskDb.tagTask(db, { id: task.id, actor: owner, add: ['needs-human'] }).tagged === true;
10664
+ if (held) taskDb.noteTask(db, { id: task.id, actor: owner, content: `Held for a person to review: ${reason}` });
10665
+ } catch {}
10666
+ }
10507
10667
  // Third failure in a row: hold the task for a person instead of letting
10508
10668
  // members pick it up again forever.
10509
- let held = false;
10510
- if (memberGoFailStreak(taskDb, db, task.id) >= MEMBER_GO_STRIKES) {
10669
+ if (!held && memberGoFailStreak(taskDb, db, task.id) >= MEMBER_GO_STRIKES) {
10511
10670
  try {
10512
10671
  held = taskDb.tagTask(db, { id: task.id, actor: owner, add: ['needs-human'] }).tagged === true;
10513
10672
  if (held) taskDb.noteTask(db, { id: task.id, actor: owner,
@@ -10516,7 +10675,7 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
10516
10675
  }
10517
10676
  const released = taskDb.releaseTask(db, { id: task.id, actor: owner });
10518
10677
  const outcome = released.released ? (held ? 'needs_human' : 'released') : 'left_claimed';
10519
- return { id: task.id, title: task.title, outcome, reason };
10678
+ return { id: task.id, title: task.title, outcome, reason, ...(held && forReview ? { held_for: 'review' } : {}) };
10520
10679
  }
10521
10680
 
10522
10681
  const MEMBER_GO_STRIKES = 3;
@@ -10595,7 +10754,7 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
10595
10754
  const missionSummary = memberMissionSummary(owner, missionMap, root);
10596
10755
  // A human or odd-typed member never takes a task: it would be claimed and
10597
10756
  // then stranded when the run refuses the member.
10598
- const pulled = pullTask && !prompt && !missionSummary.live && !memberRefusalFor(targetMember, paths.memberFile)
10757
+ const pulled = pullTask && !prompt && !missionSummary.live_mission && !memberRefusalFor(targetMember, paths.memberFile)
10599
10758
  ? memberGoTask(owner, root, { claim: !dryRun, requireCheck: needsCheck })
10600
10759
  : null;
10601
10760
  if (needsCheck && !pulled) {
@@ -10610,6 +10769,11 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
10610
10769
  || goalPlane.goal?.title
10611
10770
  || extractMemberNorthStar(paths.memberDir, '');
10612
10771
  const pulledTask = pulled ? { id: pulled.task.id, title: pulled.task.title, job: pulled.job } : null;
10772
+ // The supervisor's preview can lose a race. Only the child that claimed
10773
+ // the task may announce which job it actually got.
10774
+ if (needsCheck && pulledTask && !dryRun && process.connected && process.send) {
10775
+ process.send({ type: 'member_task_claimed', member: owner, task: pulledTask }, () => {});
10776
+ }
10613
10777
  if (pulled) whyPicked = `${whyPicked} Took the oldest open ${pulled.job} task from the list.`.trim();
10614
10778
  const engineSeat = resolveMemberEngineSeat(owner, root);
10615
10779
  const { autoQueueMemberNextPush, closeMemberHandoffsForReceiver } = require('./mission');
@@ -10704,7 +10868,8 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
10704
10868
  const runFlags = required ? [...stripKnownFlags(nextFlags, ['--verify'], []), '--verify', required] : nextFlags;
10705
10869
  let result = null;
10706
10870
  try {
10707
- result = await memberRunWith(owner, [effectivePrompt, '--ignite', ...runFlags], { quiet: asJson });
10871
+ // The job rides to the merge line so it closes itself when its change merges.
10872
+ result = await memberRunWith(owner, [effectivePrompt, '--ignite', ...runFlags], { quiet: asJson, closeTaskIds: [pulled.task.id] });
10708
10873
  } catch (error) {
10709
10874
  memberGoFinishTask(owner, pulled.task, null, error);
10710
10875
  throw error;
@@ -10715,6 +10880,8 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
10715
10880
  ? { task, work_step: { summary: result?.work_step?.summary || null }, error: result?.error || null }
10716
10881
  : merged));
10717
10882
  else if (task.outcome === 'review') console.log(`Task "${task.title}" is ready for review.`);
10883
+ else if (task.outcome === 'done') console.log(`Task "${task.title}" merged and closed itself.`);
10884
+ else if (task.outcome === 'needs_human' && task.held_for === 'review') console.log(`Task "${task.title}" waits for a person to review it: ${task.reason}`);
10718
10885
  else if (task.outcome === 'needs_human') console.log(`Task "${task.title}" failed three times in a row and now waits for a person: ${task.reason}`);
10719
10886
  else console.log(`Task "${task.title}" went back on the list: ${task.reason}`);
10720
10887
  return merged;
@@ -10788,14 +10955,42 @@ function memberUpPick(root, name, taken) {
10788
10955
  const { fittingTasks } = require('../lib/member-task-pick');
10789
10956
  const db = taskDb.open();
10790
10957
  const job = memberGoJob(name, root);
10791
- return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), job, requireCheck: true })
10958
+ return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), member: name, job, requireCheck: true, recoverClaimed: true })
10792
10959
  .find((task) => !taken.has(task.id)) || null;
10793
10960
  } catch {
10794
10961
  return null;
10795
10962
  }
10796
10963
  }
10797
10964
 
10798
- function memberUpSpawn(root, name, onExit) {
10965
+ // An empty job list: write the member's next job from its own mission.
10966
+ function memberUpWriteNextJob(root, name, { dryRun = false } = {}) {
10967
+ const taskDb = require('../lib/task-db');
10968
+ const { writeSelfNextJob } = require('../lib/member-self-next-job');
10969
+ const db = taskDb.open();
10970
+ return writeSelfNextJob(taskDb, db, {
10971
+ root,
10972
+ workspaceRoot: taskDb.workspaceRoot(root),
10973
+ member: name,
10974
+ job: memberGoJob(name, root),
10975
+ dryRun,
10976
+ });
10977
+ }
10978
+
10979
+ // Every open task a member could start, with the job it needs.
10980
+ function memberUpWaiting(root) {
10981
+ try {
10982
+ const taskDb = require('../lib/task-db');
10983
+ const { fittingTasks } = require('../lib/member-task-pick');
10984
+ const db = taskDb.open();
10985
+ const workspaceRoot = taskDb.workspaceRoot(root);
10986
+ return ['build', 'review', 'search'].flatMap((job) => fittingTasks(taskDb, db, { workspaceRoot, job, requireCheck: true })
10987
+ .map((task) => ({ id: task.id, title: task.title, job })));
10988
+ } catch {
10989
+ return [];
10990
+ }
10991
+ }
10992
+
10993
+ function memberUpSpawn(root, name, onExit, onClaim) {
10799
10994
  const { spawn } = require('child_process');
10800
10995
  const logDir = path.join(root, '.atris', 'state', 'member-up');
10801
10996
  fs.mkdirSync(logDir, { recursive: true });
@@ -10808,12 +11003,15 @@ function memberUpSpawn(root, name, onExit) {
10808
11003
  cwd: root,
10809
11004
  detached: true,
10810
11005
  // Children keep their log handle when the foreground supervisor stops.
10811
- stdio: ['ignore', out, out],
11006
+ stdio: ['ignore', out, out, 'ipc'],
10812
11007
  env: process.env,
10813
11008
  });
10814
11009
  } finally {
10815
11010
  fs.closeSync(out);
10816
11011
  }
11012
+ child.on('message', (message) => {
11013
+ if (message?.type === 'member_task_claimed' && message.member === name) onClaim?.(message.task);
11014
+ });
10817
11015
  child.on('error', (error) => onExit(null, { ok: false, reason: error.message }));
10818
11016
  child.on('close', (code, signal) => {
10819
11017
  let output = '';
@@ -10838,7 +11036,11 @@ async function memberUp(...args) {
10838
11036
  if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
10839
11037
  console.log('Usage: atris member up [--max N] [--only a,b] [--dry-run]');
10840
11038
  console.log('Keeps the team working: every minute, starts each free member that has a fitting task with a runnable check.');
10841
- console.log('A member with no such task stays idle; it never runs its own goal instead.');
11039
+ console.log('A member with no such task writes its next job from its mission when the mission has a ## Check, then starts it.');
11040
+ console.log('Without a check it stays idle. At most one self-written job is open per member, and 3 a day.');
11041
+ console.log('The longer a member stays idle, the less often it is checked: 5, 10, 20, then every 30 minutes.');
11042
+ console.log('A working member with no new step for 20 minutes is called out as stuck, once until it moves again.');
11043
+ console.log('Every 5 minutes it also puts member changes github dropped from its merge line back in, when self-landing is on.');
10842
11044
  console.log('--only limits it to those members. Same as atris member wake 5 or atris member wake a b.');
10843
11045
  console.log('Ctrl-C or atris member sleep --all ends it. Runs it started keep going.');
10844
11046
  return;
@@ -10855,8 +11057,17 @@ async function memberUp(...args) {
10855
11057
  const deps = {
10856
11058
  listMembers: () => memberUpTeam(root),
10857
11059
  pickTask: (name, taken) => memberUpPick(root, name, taken),
10858
- spawn: (name, onExit) => memberUpSpawn(root, name, onExit),
11060
+ writeNextJob: (name, opts) => memberUpWriteNextJob(root, name, opts),
11061
+ spawn: (name, onExit, onClaim) => memberUpSpawn(root, name, onExit, onClaim),
10859
11062
  now: () => Date.now(),
11063
+ jobOf: (name) => memberGoJob(name, root),
11064
+ waitingTasks: () => memberUpWaiting(root),
11065
+ requeueDropped: () => require('../lib/member-up').memberMaintenance({
11066
+ cwd: root,
11067
+ codeRoot: path.resolve(__dirname, '..'),
11068
+ requeueDropped: () => require('../lib/member-autoland').requeueDroppedAutolands({ cwd: root }),
11069
+ waitingTasks: () => memberUpWaiting(root),
11070
+ }),
10860
11071
  };
10861
11072
  if (parsed.dryRun) {
10862
11073
  const up = createMemberUp(deps, { max, only, dryRun: true });
@@ -10957,16 +11168,56 @@ function memberSleep(target, ...args) {
10957
11168
  return payload;
10958
11169
  }
10959
11170
 
11171
+ // The member's active goal plus how many times it was set, so a re-agreed
11172
+ // goal with the same words still counts as the talk's next step.
11173
+ function oneOnOneGoalMark(name, paths) {
11174
+ const goal = (loadMemberGoals(name, paths).goals || []).filter((g) => g && g.status === 'active').pop();
11175
+ return goal ? { title: goal.title, mark: `${goal.id}:${(goal.history || []).length}` } : { title: '', mark: '' };
11176
+ }
11177
+
11178
+ function askOneOnOneNextStep(name) {
11179
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
11180
+ return new Promise((resolve) => rl.question(`What is ${name}'s next step? `, (answer) => {
11181
+ rl.close();
11182
+ resolve(String(answer || '').trim());
11183
+ }));
11184
+ }
11185
+
11186
+ // A 1-on-1 is a talk with the member in its own voice, and it ends with the
11187
+ // member's next step saved as its active goal. The member records the step
11188
+ // itself during the talk; --next or a closing question fills it in otherwise.
10960
11189
  async function memberOneOnOne(name, ...args) {
10961
11190
  if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
10962
- console.log('Usage: atris member 1on1 <name> [--note "..."]');
10963
- console.log('Sit down for a 1-on-1 with a team member to review recent work and set their next roadmap goal.');
11191
+ console.log('Usage: atris member 1on1 <name> [--next "..."]');
11192
+ console.log('Talk with a team member in its own voice. The talk ends with its next step saved as its goal.');
10964
11193
  return;
10965
11194
  }
11195
+ const paths = requireMemberDir(name);
11196
+ const next = readFlag(args, '--next', '');
11197
+ const rest = stripKnownFlags(args, ['--next'], []);
11198
+ const before = oneOnOneGoalMark(name, paths);
10966
11199
  const { interviewCommand } = require('./interview');
10967
- const code = await interviewCommand([name, ...args]);
10968
- if (code) process.exitCode = code;
10969
- return code;
11200
+ const code = await interviewCommand([name, ...rest], process.cwd(), { mode: 'one_on_one' });
11201
+ if (code) {
11202
+ process.exitCode = code;
11203
+ return code;
11204
+ }
11205
+ let after = oneOnOneGoalMark(name, paths);
11206
+ if (after.mark === before.mark) {
11207
+ const step = next || (process.stdin.isTTY && process.stdout.isTTY ? await askOneOnOneNextStep(name) : '');
11208
+ if (step) {
11209
+ memberGoal(name, step);
11210
+ after = oneOnOneGoalMark(name, paths);
11211
+ }
11212
+ }
11213
+ if (after.title && after.mark !== before.mark) {
11214
+ console.log(`${name}'s next step: ${after.title}`);
11215
+ return 0;
11216
+ }
11217
+ console.log(`The 1-on-1 ended without a next step for ${name}.`);
11218
+ console.log(`Set one: atris member goal ${name} "..."`);
11219
+ process.exitCode = 1;
11220
+ return 1;
10970
11221
  }
10971
11222
 
10972
11223
  function collectMemberHandoffs(root = process.cwd(), cards = []) {
@@ -11315,6 +11566,9 @@ async function memberCommand(subcommand, ...args) {
11315
11566
  case 'list':
11316
11567
  case 'ls':
11317
11568
  return memberList(...args);
11569
+ case 'roster':
11570
+ // Who is on the team, working or idle, and on which engine.
11571
+ return require('./team').memberRosterCommand(args);
11318
11572
  case 'create':
11319
11573
  case 'new':
11320
11574
  return memberCreate(args[0], ...args.slice(1));
@@ -11440,6 +11694,14 @@ async function memberCommand(subcommand, ...args) {
11440
11694
  if (isKnownMemberSlug(subcommand, process.cwd()) && args.filter((a) => !String(a).startsWith('-')).length === 0) {
11441
11695
  return memberCard(subcommand, ...args);
11442
11696
  }
11697
+ // One word that is neither a member nor a subcommand is a typo far
11698
+ // more often than a job. Starting a paid run on it costs money and
11699
+ // does nothing useful, so name the miss and stop. Jobs keep working
11700
+ // as more than one word, or through `atris member run "<job>"`.
11701
+ const extraWords = args.filter((a) => !String(a).startsWith('-')).length;
11702
+ if (!/\s/.test(subcommand.trim()) && extraWords === 0) {
11703
+ return memberUnknownWord(subcommand);
11704
+ }
11443
11705
  return memberSteerOrDispatch(subcommand, ...args);
11444
11706
  }
11445
11707
  console.log('run "<job>" pick the right member and run it: atris member run "fix the flaky login test"');
@@ -11453,14 +11715,15 @@ async function memberCommand(subcommand, ...args) {
11453
11715
  console.log(' "<prompt>" Steer a running member mid-flight or start the best specialist');
11454
11716
  console.log(' go [name|"..."] Wake the highest-leverage member on its next_push or run a prompt');
11455
11717
  console.log(' steer [name] "..." Steer a member mid-run or update its immediate direction');
11456
- console.log(' 1on1 <name> Sit down for a 1-on-1 to review recent wins and set the roadmap');
11718
+ console.log(' 1on1 <name> Talk with a member in its own voice; ends with its next step saved');
11457
11719
  console.log(' sleep [name|--all] Park active member loops so your team rests until woken');
11458
11720
  console.log(' room [--json] Render the visual Team Room and desktop UI payload');
11459
11721
  console.log(' board [--watch] One screen of every member: job, last step, working/quiet/done');
11460
11722
  console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
11461
11723
  console.log(' install <slug> [--update] Install missing files, or refresh template skills');
11462
11724
  console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
11463
- console.log(' list [--json] Show all team members');
11725
+ console.log(' list [--json] Show all team members with skills and version');
11726
+ console.log(' roster [--all] Who is on the team: what each does, working or idle, and engine');
11464
11727
  console.log(' tidy [--dry-run] Put away member copies of the repo whose work already landed');
11465
11728
  console.log(' export [--dry-run] Write each member as a subagent for Claude Code, Cursor, and Codex');
11466
11729
  console.log(' activate <name> Symlink member skills, show context and permissions');
@@ -11541,6 +11804,8 @@ module.exports = {
11541
11804
  memberRunWantsReferee,
11542
11805
  proposalPromptForGoal,
11543
11806
  memberGoFinishTask,
11807
+ memberRunReviewAsk,
11808
+ memberUpSpawn,
11544
11809
  appendMemberGoalLog,
11545
11810
  appendProjectLog,
11546
11811
  memberCommand,