atris 3.68.8 → 3.69.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.
@@ -1017,6 +1017,12 @@ function startMemberRunMission(name, missionText, args = []) {
1017
1017
  return memberRunCli(startArgs, process.cwd());
1018
1018
  }
1019
1019
 
1020
+ // An ignited run asks a second engine to review the work by default, so every
1021
+ // run ends with a verdict. --no-referee skips it; an explicit --referee is kept.
1022
+ function memberRunWantsReferee(runArgs, ignite) {
1023
+ return Boolean(ignite) && !hasFlag(runArgs, '--referee') && !hasFlag(runArgs, '--no-referee');
1024
+ }
1025
+
1020
1026
  function memberRunCli(args, cwd, { live = false, missionId = '' } = {}) {
1021
1027
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
1022
1028
  const runsDir = path.join(cwd, 'atris', 'runs');
@@ -1232,6 +1238,7 @@ function memberRun(name, ...args) {
1232
1238
  function memberRunWith(name, args, { quiet = false } = {}) {
1233
1239
  if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
1234
1240
  console.log('usage: atris member run <name> ["mission text"] [--verify "<cmd>"] [--minutes N|--hours N] [--json]');
1241
+ console.log('pick for me: atris member run "fix the flaky login test" (the best member takes it and runs it)');
1235
1242
  console.log('new mission: atris member run growth "improve onboarding proof" --verify "npm test" --minutes 30 --json');
1236
1243
  console.log('choose work: atris member run growth --verify "npm test" --industry logistics --minutes 30 --json');
1237
1244
  console.log('subagent shell: atris member run growth "improve onboarding proof" --engine claude --ignite --json');
@@ -1240,6 +1247,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1240
1247
  console.log('truth: useful work must improve revenue, reliability, speed, security, clarity, or trust, then show plain proof.');
1241
1248
  console.log('isolation: new missions use --worktree by default; add --shared-checkout to stay here.');
1242
1249
  console.log('check: --verify or a verify field in the member file adds a mission check.');
1250
+ console.log('review: an --ignite run has a different engine review the work; add --no-referee to skip it.');
1243
1251
  return;
1244
1252
  }
1245
1253
 
@@ -1305,6 +1313,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1305
1313
  const progressDir = process.env.ATRIS_PROGRESS_DIR || '~/.atris/progress';
1306
1314
  console.log(`watch: tail -f ${progressDir}/${String(name).replace(/[^A-Za-z0-9._-]/g, '_')}.log`);
1307
1315
  console.log('board: atris member board --watch');
1316
+ console.log('stop: atris member stop <name>');
1308
1317
  }
1309
1318
 
1310
1319
  const cwd = mission.worktree?.path || process.cwd();
@@ -1324,6 +1333,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1324
1333
  ['--mission', '--mission-id', '--minutes', '--hours', '--industry', '--domain', '--value', '--outcome', '--truth', '--proof', '--base'],
1325
1334
  ['--worktree', '--shared-checkout', '--no-worktree', '--choose-work', '--auto', '--ignite', '--wait', '--sync']);
1326
1335
  if (ignite && !hasFlag(runArgs, '--complete-on-pass')) runArgs.push('--complete-on-pass');
1336
+ if (memberRunWantsReferee(runArgs, ignite)) runArgs.push('--referee');
1327
1337
  const budgetSeconds = memberRunBudgetSeconds(args);
1328
1338
  if (!readFlag(runArgs, '--max-ticks', '')) {
1329
1339
  runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
@@ -1365,6 +1375,16 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1365
1375
  result.ask = `${name} is waiting on you: ${payload.human_asks[0]}`;
1366
1376
  result.next_command = payload.next_command || 'atris decide';
1367
1377
  }
1378
+ // A finished ignited run whose check passed hands its branch to GitHub
1379
+ // auto-merge, only past the local gates; any held gate leaves the branch.
1380
+ if (ignite && ok && started && !reviewAsk && finalMission.verifier_result?.passed === true) {
1381
+ const ticks = Array.isArray(payload.ticks) ? payload.ticks : [];
1382
+ const summary = ticks.map((tick) => tick?.summary || tick?.claude?.summary).filter(Boolean).pop() || '';
1383
+ const workerEngine = ticks.map((tick) => tick?.engine_id).filter(Boolean).pop()
1384
+ || runnerSwap?.to || readFlag(args, '--runner', '') || '';
1385
+ result.autoland = require('../lib/member-autoland').autolandMemberRun({ mission: finalMission, cwd, summary, workerEngine });
1386
+ if (!asJson && !quiet) console.log(result.autoland.line);
1387
+ }
1368
1388
  if (!ok) {
1369
1389
  result.error = result.ask || payload.detail || payload.error || payload.reason || payload.next_action
1370
1390
  || run.stderr || 'mission did not run a work step';
@@ -9796,11 +9816,35 @@ const IDEA_ARCHETYPE_RULES = [
9796
9816
  },
9797
9817
  ];
9798
9818
 
9799
- // Dispatch defaults to the core team. Other members (experiments, one-off
9800
- // workers) win only when the idea names them or out-scores the core by 2x.
9801
- const CORE_DISPATCH_MEMBERS = new Set([
9802
- 'executor', 'architect', 'auto-improver', 'navigator', 'validator', 'customer-lead', 'researcher',
9803
- ]);
9819
+ // Keep specialized idea rules, then fall back to the same job inference used
9820
+ // by the engine roster. Renamed members get the same job score as built-ins.
9821
+ const ARCHETYPE_FOR_JOB = { build: 'executor', review: 'validator', search: 'researcher' };
9822
+ const EXPERIMENT_STATUSES = new Set(['experimental', 'one-off']);
9823
+
9824
+ function ideaRuleFor(slug, record, text) {
9825
+ const own = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(slug));
9826
+ if (own && own.pattern.test(text)) return own;
9827
+ const { autoJobForMember } = require('../lib/member-engine');
9828
+ const archetype = ARCHETYPE_FOR_JOB[autoJobForMember(slug, record.role)];
9829
+ return IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(archetype)) || null;
9830
+ }
9831
+
9832
+ // 0 = normal, 1 = experiment or one-off, 2 = parked, human, or odd-typed.
9833
+ function dispatchTier(record) {
9834
+ const frontmatter = record.frontmatter || {};
9835
+ const status = String(frontmatter.status || '').trim().toLowerCase();
9836
+ if (status === 'parked' || memberRefusalReason(memberSlugFromRecord(record), frontmatter)) return 2;
9837
+ if (EXPERIMENT_STATUSES.has(status)) return 1;
9838
+ return 0;
9839
+ }
9840
+
9841
+ // Lower tier wins first, then higher score, then name order for a stable pick.
9842
+ function outranksDispatch(candidate, best) {
9843
+ if (!best) return true;
9844
+ if (candidate.tier !== best.tier) return candidate.tier < best.tier;
9845
+ if (candidate.score !== best.score) return candidate.score > best.score;
9846
+ return candidate.member.localeCompare(best.member) < 0;
9847
+ }
9804
9848
 
9805
9849
  function ideaNamesMember(text, slug) {
9806
9850
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/-/g, '[-\\s]+');
@@ -9835,15 +9879,17 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9835
9879
  let best = null;
9836
9880
  for (const record of members) {
9837
9881
  const slug = memberSlugFromRecord(record);
9882
+ const named = ideaNamesMember(text, slug);
9883
+ // Naming an experiment in the idea is the owner's pick, so it ranks normally.
9884
+ const baseTier = dispatchTier(record);
9885
+ const tier = named && baseTier === 1 ? 0 : baseTier;
9838
9886
  let score = 0;
9839
9887
  let matchedReason = '';
9840
9888
 
9841
- for (const rule of IDEA_ARCHETYPE_RULES) {
9842
- if (rule.slugs.includes(slug) && rule.pattern.test(text)) {
9843
- score += 25 + (rule.slugs[0] === slug ? 5 : 0);
9844
- matchedReason = rule.reason;
9845
- break;
9846
- }
9889
+ const rule = ideaRuleFor(slug, record, text);
9890
+ if (rule && rule.pattern.test(text)) {
9891
+ score += 30;
9892
+ matchedReason = rule.reason;
9847
9893
  }
9848
9894
 
9849
9895
  const missionFile = path.join(teamDir, slug, 'MISSION.md');
@@ -9856,14 +9902,10 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9856
9902
  }
9857
9903
  score += overlapCount * 4;
9858
9904
  if (Number(record.skillCount || 0) > 0) score += 2;
9859
- if (slug === 'executor') score += 3;
9860
- if (slug === 'auto-improver') score += 2;
9861
9905
 
9862
- if (ideaNamesMember(text, slug)) {
9906
+ if (named) {
9863
9907
  score += 40;
9864
9908
  matchedReason = 'named in the idea';
9865
- } else if (!CORE_DISPATCH_MEMBERS.has(slug)) {
9866
- score = Math.floor(score / 2);
9867
9909
  }
9868
9910
 
9869
9911
  if (!matchedReason) {
@@ -9877,10 +9919,9 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9877
9919
  role: record.role || 'Team Member',
9878
9920
  reason: matchedReason,
9879
9921
  score,
9922
+ tier,
9880
9923
  };
9881
- if (!best || candidate.score > best.score || (candidate.score === best.score && candidate.member.localeCompare(best.member) < 0)) {
9882
- best = candidate;
9883
- }
9924
+ if (outranksDispatch(candidate, best)) best = candidate;
9884
9925
  }
9885
9926
 
9886
9927
  return best;
@@ -9938,7 +9979,7 @@ function memberDispatch(...args) {
9938
9979
  printJsonOrText(
9939
9980
  payload,
9940
9981
  [
9941
- `${picked.member} (${picked.role}) will take this because it ${picked.reason.replace(/^[A-Z]/, (c) => c.toLowerCase())}.`,
9982
+ `${picked.member} (${picked.role}) will take this: ${picked.reason.replace(/\.$/, '')}.`,
9942
9983
  '',
9943
9984
  commandPreview,
9944
9985
  ],
@@ -9948,7 +9989,7 @@ function memberDispatch(...args) {
9948
9989
  }
9949
9990
 
9950
9991
  if (!asJson) {
9951
- console.log(`${picked.member} (${picked.role}) is taking this because it ${picked.reason.replace(/^[A-Z]/, (c) => c.toLowerCase())}.`);
9992
+ console.log(`${picked.member} (${picked.role}) is taking this: ${picked.reason.replace(/\.$/, '')}.`);
9952
9993
  }
9953
9994
  // --ignite means do the work now, the same as the previewed member run;
9954
9995
  // without it dispatch only opens the mission.
@@ -10371,7 +10412,70 @@ function isKnownMemberSlug(token, root = process.cwd()) {
10371
10412
  || fs.existsSync(path.join(root, 'atris', 'team', `${clean}.md`));
10372
10413
  }
10373
10414
 
10415
+ // The job a member does (build, review or search), read from the same team
10416
+ // roster that picks its engine.
10417
+ function memberGoJob(owner, root) {
10418
+ const byRole = { navigator: 'search', executor: 'build', validator: 'review' };
10419
+ try {
10420
+ const seat = require('../lib/member-engine').resolveEngineForMember(owner, root);
10421
+ return byRole[seat.role] || byRole[seat.job_key] || 'build';
10422
+ } catch {
10423
+ return 'build';
10424
+ }
10425
+ }
10426
+
10427
+ // With no idea given, member go takes the next real job from the task list:
10428
+ // the oldest open task that fits the member's job. A dry run only looks.
10429
+ // requireCheck: only tasks with a runnable check count.
10430
+ function memberGoTask(owner, root, { claim, requireCheck = false }) {
10431
+ try {
10432
+ const taskDb = require('../lib/task-db');
10433
+ const picker = require('../lib/member-task-pick');
10434
+ const db = taskDb.open();
10435
+ const job = memberGoJob(owner, root);
10436
+ const workspaceRoot = taskDb.workspaceRoot(root);
10437
+ if (claim) {
10438
+ const got = picker.claimTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck });
10439
+ return got ? { ...got, job } : null;
10440
+ }
10441
+ const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, job, requireCheck });
10442
+ return task ? { task, objective: picker.taskObjective(task), job } : null;
10443
+ } catch (error) {
10444
+ console.error(`could not take a task from the list: ${String(error && error.message || error).split('\n')[0]}`);
10445
+ return null;
10446
+ }
10447
+ }
10448
+
10449
+ // A pulled task leaves the run in review when the run's check passed (the
10450
+ // mission is complete, or ready: checked and waiting on a person), else back
10451
+ // on the list with a one-line reason.
10452
+ function memberGoFinishTask(owner, task, result, thrown = null) {
10453
+ const taskDb = require('../lib/task-db');
10454
+ const db = taskDb.open();
10455
+ const mission = (result && result.mission) || {};
10456
+ const receipt = (result && result.work_step && result.work_step.receipt_path) || mission.receipt_path || '';
10457
+ const required = require('../lib/member-task-pick').taskCheck(task).command;
10458
+ const checked = mission.verifier_result && mission.verifier_result.passed === true
10459
+ && (!required || String(mission.verifier_result.command || '').trim() === required);
10460
+ if (!thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked) {
10461
+ const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
10462
+ receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
10463
+ const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof });
10464
+ if (ready.ready !== false) return { id: task.id, title: task.title, outcome: 'review', receipt: receipt || null };
10465
+ }
10466
+ const why = thrown ? thrown.message
10467
+ : (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
10468
+ const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
10469
+ try { taskDb.noteTask(db, { id: task.id, actor: owner, content: `Released by ${owner}: ${reason}` }); } catch {}
10470
+ const released = taskDb.releaseTask(db, { id: task.id, actor: owner });
10471
+ return { id: task.id, title: task.title, outcome: released.released ? 'released' : 'left_claimed', reason };
10472
+ }
10473
+
10374
10474
  async function memberSteerOrDispatch(...rawArgs) {
10475
+ return memberSteerOrDispatchWith({}, rawArgs);
10476
+ }
10477
+
10478
+ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []) {
10375
10479
  if (hasFlag(rawArgs, '--help') || hasFlag(rawArgs, '-h')) {
10376
10480
  console.log('Usage: atris member steer [member] "<direction>" [--dry-run] [--json]');
10377
10481
  console.log(' atris member "<direction>" [--dry-run] [--json]');
@@ -10381,6 +10485,9 @@ async function memberSteerOrDispatch(...rawArgs) {
10381
10485
  const root = process.cwd();
10382
10486
  const asJson = hasFlag(rawArgs, '--json');
10383
10487
  const dryRun = hasFlag(rawArgs, '--dry-run');
10488
+ // Set by `member up`: run only a claimed task with a runnable check, never
10489
+ // the member's own goal text.
10490
+ const needsCheck = pullTask && hasFlag(rawArgs, '--needs-check');
10384
10491
  const valueFlags = new Set(['--engine', '--model', '--verify', '--referee-engine', '--minutes', '--hours', '--max-ticks', '--from']);
10385
10492
  const positionals = [];
10386
10493
  const flagArgs = [];
@@ -10388,6 +10495,7 @@ async function memberSteerOrDispatch(...rawArgs) {
10388
10495
  const token = String(rawArgs[i] || '');
10389
10496
  if (!token) continue;
10390
10497
  if (token.startsWith('-')) {
10498
+ if (token === '--needs-check') continue;
10391
10499
  flagArgs.push(token);
10392
10500
  if (valueFlags.has(token) && i + 1 < rawArgs.length) {
10393
10501
  flagArgs.push(rawArgs[i + 1]);
@@ -10419,18 +10527,31 @@ async function memberSteerOrDispatch(...rawArgs) {
10419
10527
  fm = parseFrontmatter(fs.readFileSync(paths.memberFile, 'utf8')) || {};
10420
10528
  } catch {}
10421
10529
  const goalPlane = memberGoalPlaneStatus(owner, paths);
10530
+ const missionMap = memberRunMissionMap(root);
10531
+ const missionSummary = memberMissionSummary(owner, missionMap, root);
10532
+ // A human or odd-typed member never takes a task: it would be claimed and
10533
+ // then stranded when the run refuses the member.
10534
+ const pulled = pullTask && !prompt && !missionSummary.live && !memberRefusalFor(targetMember, paths.memberFile)
10535
+ ? memberGoTask(owner, root, { claim: !dryRun, requireCheck: needsCheck })
10536
+ : null;
10537
+ if (needsCheck && !pulled) {
10538
+ const payload = { ok: true, action: 'member_go', mode: 'idle', member: owner, reason: 'no task with a check' };
10539
+ printJsonOrText(payload, [`${owner} is idle, no task with a check`], asJson);
10540
+ process.exitCode = require('../lib/member-up').IDLE_EXIT;
10541
+ return payload;
10542
+ }
10422
10543
  const effectivePrompt = prompt
10544
+ || pulled?.objective
10423
10545
  || goalPlane.goal?.next_push
10424
10546
  || goalPlane.goal?.title
10425
10547
  || extractMemberNorthStar(paths.memberDir, '');
10426
-
10427
- const missionMap = memberRunMissionMap(root);
10428
- const missionSummary = memberMissionSummary(owner, missionMap, root);
10548
+ const pulledTask = pulled ? { id: pulled.task.id, title: pulled.task.title, job: pulled.job } : null;
10549
+ if (pulled) whyPicked = `${whyPicked} Took the oldest open ${pulled.job} task from the list.`.trim();
10429
10550
  const engineSeat = resolveMemberEngineSeat(owner, root);
10430
10551
  const { autoQueueMemberNextPush, closeMemberHandoffsForReceiver } = require('./mission');
10431
10552
  const subagentShell = buildMemberSubagentShell(owner, fm.role || 'Team Member', engineSeat.engine_id, effectivePrompt, root);
10432
10553
 
10433
- if (effectivePrompt) {
10554
+ if (effectivePrompt && !pulled) {
10434
10555
  try {
10435
10556
  autoQueueMemberNextPush(owner, effectivePrompt, missionSummary.live?.objective || '', root);
10436
10557
  } catch {}
@@ -10494,11 +10615,13 @@ async function memberSteerOrDispatch(...rawArgs) {
10494
10615
  engine_reason: engineSeat.reason,
10495
10616
  prompt: effectivePrompt,
10496
10617
  why: whyPicked,
10618
+ task: pulledTask,
10497
10619
  command,
10498
10620
  subagent_shell: subagentShell,
10499
10621
  };
10500
10622
  const lines = [
10501
10623
  `Routed to ${owner} on engine ${engineSeat.engine_id}: ${whyPicked}`,
10624
+ ...(pulledTask ? [`Task: ${pulledTask.title}`] : []),
10502
10625
  `Objective: ${effectivePrompt}`,
10503
10626
  `Next: ${command}`,
10504
10627
  ];
@@ -10511,7 +10634,25 @@ async function memberSteerOrDispatch(...rawArgs) {
10511
10634
  }
10512
10635
  const hasExplicitEngine = flagArgs.includes('--engine');
10513
10636
  const nextFlags = hasExplicitEngine ? flagArgs : ['--engine', engineSeat.engine_id, ...flagArgs];
10514
- return memberRun(owner, effectivePrompt, '--ignite', ...nextFlags);
10637
+ if (!pulled) return memberRun(owner, effectivePrompt, '--ignite', ...nextFlags);
10638
+ // A pulled task runs with the check it requires, not the member's own.
10639
+ const required = require('../lib/member-task-pick').taskCheck(pulled.task).command;
10640
+ const runFlags = required ? [...stripKnownFlags(nextFlags, ['--verify'], []), '--verify', required] : nextFlags;
10641
+ let result = null;
10642
+ try {
10643
+ result = await memberRunWith(owner, [effectivePrompt, '--ignite', ...runFlags], { quiet: asJson });
10644
+ } catch (error) {
10645
+ memberGoFinishTask(owner, pulled.task, null, error);
10646
+ throw error;
10647
+ }
10648
+ const task = memberGoFinishTask(owner, pulled.task, result);
10649
+ const merged = { ...(result || {}), task };
10650
+ if (asJson) console.log(JSON.stringify(needsCheck
10651
+ ? { task, work_step: { summary: result?.work_step?.summary || null }, error: result?.error || null }
10652
+ : merged));
10653
+ else if (task.outcome === 'review') console.log(`Task "${task.title}" is ready for review.`);
10654
+ else console.log(`Task "${task.title}" went back on the list: ${task.reason}`);
10655
+ return merged;
10515
10656
  }
10516
10657
 
10517
10658
  async function memberGo(...rawArgs) {
@@ -10522,7 +10663,7 @@ async function memberGo(...rawArgs) {
10522
10663
  }
10523
10664
  const nonFlags = rawArgs.filter((a) => a && !String(a).startsWith('-'));
10524
10665
  if (nonFlags.length > 0) {
10525
- return memberSteerOrDispatch(...rawArgs);
10666
+ return memberSteerOrDispatchWith({ pullTask: true }, rawArgs);
10526
10667
  }
10527
10668
  const root = process.cwd();
10528
10669
  const teamDir = path.join(root, 'atris', 'team');
@@ -10542,7 +10683,155 @@ async function memberGo(...rawArgs) {
10542
10683
  const fallback = findAllMembers(teamDir).filter(isFleetMember)[0];
10543
10684
  if (fallback) chosen = memberSlugFromRecord(fallback);
10544
10685
  }
10545
- return memberSteerOrDispatch(chosen, ...rawArgs);
10686
+ return memberSteerOrDispatchWith({ pullTask: true }, [chosen, ...rawArgs]);
10687
+ }
10688
+
10689
+ // Where a running `atris member up` leaves its pid, so a whole-team sleep
10690
+ // can end it.
10691
+ function memberUpPidFile(root) {
10692
+ return path.join(root, '.atris', 'state', 'member-up.json');
10693
+ }
10694
+
10695
+ // A member slept by name stays asleep for member up until someone wakes it;
10696
+ // a whole-team sleep only rests the team for now.
10697
+ function memberSleepingByChoice(latest) {
10698
+ return Boolean(latest && latest.status === 'paused' && latest.stop_reason === 'member-sleep'
10699
+ && latest.sleep_target === 'member');
10700
+ }
10701
+
10702
+ function memberUpTeam(root) {
10703
+ const { isMemberParked } = require('../lib/member-park');
10704
+ const missionMap = memberRunMissionMap(root);
10705
+ return findAllMembers(path.join(root, 'atris', 'team')).filter(isFleetMember).map((record) => {
10706
+ const name = memberSlugFromRecord(record);
10707
+ const summary = memberMissionSummary(name, missionMap, root);
10708
+ return {
10709
+ name,
10710
+ parked: isMemberParked(root, name),
10711
+ human: Boolean(memberRefusalFor(name, path.join(root, 'atris', 'team', name, 'MEMBER.md'))),
10712
+ sleeping: memberSleepingByChoice(summary.latest),
10713
+ live: Boolean(summary.live_mission),
10714
+ };
10715
+ });
10716
+ }
10717
+
10718
+ // The task `member go <name>` would claim, skipping ones already handed out.
10719
+ // Looks only; the child claims it.
10720
+ function memberUpPick(root, name, taken) {
10721
+ try {
10722
+ const taskDb = require('../lib/task-db');
10723
+ const { fittingTasks } = require('../lib/member-task-pick');
10724
+ const db = taskDb.open();
10725
+ const job = memberGoJob(name, root);
10726
+ return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), job, requireCheck: true })
10727
+ .find((task) => !taken.has(task.id)) || null;
10728
+ } catch {
10729
+ return null;
10730
+ }
10731
+ }
10732
+
10733
+ function memberUpSpawn(root, name, onExit) {
10734
+ const { spawn } = require('child_process');
10735
+ const logDir = path.join(root, '.atris', 'state', 'member-up');
10736
+ fs.mkdirSync(logDir, { recursive: true });
10737
+ const logFile = path.join(logDir, `${name}.log`);
10738
+ const out = fs.openSync(logFile, 'a');
10739
+ const start = fs.fstatSync(out).size;
10740
+ let child;
10741
+ try {
10742
+ child = spawn(process.execPath, [path.join(__dirname, '..', 'bin', 'atris.js'), 'member', 'go', name, '--needs-check', '--json'], {
10743
+ cwd: root,
10744
+ detached: true,
10745
+ // Children keep their log handle when the foreground supervisor stops.
10746
+ stdio: ['ignore', out, out],
10747
+ env: process.env,
10748
+ });
10749
+ } finally {
10750
+ fs.closeSync(out);
10751
+ }
10752
+ child.on('error', (error) => onExit(null, { ok: false, reason: error.message }));
10753
+ child.on('close', (code, signal) => {
10754
+ let output = '';
10755
+ let fd;
10756
+ try {
10757
+ fd = fs.openSync(logFile, 'r');
10758
+ const size = fs.fstatSync(fd).size;
10759
+ const from = Math.max(start, size - 64 * 1024);
10760
+ const buf = Buffer.alloc(Math.max(0, size - from));
10761
+ const read = fs.readSync(fd, buf, 0, buf.length, from);
10762
+ output = buf.subarray(0, read).toString('utf8');
10763
+ } catch {} finally {
10764
+ if (fd !== undefined) fs.closeSync(fd);
10765
+ }
10766
+ const { memberUpResult } = require('../lib/member-up');
10767
+ onExit(code, memberUpResult(output, '', signal));
10768
+ });
10769
+ return child;
10770
+ }
10771
+
10772
+ async function memberUp(...args) {
10773
+ if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
10774
+ console.log('Usage: atris member up [--max N] [--only a,b] [--dry-run]');
10775
+ console.log('Keeps the team working: every minute, starts each free member that has a fitting task with a runnable check.');
10776
+ console.log('A member with no such task stays idle; it never runs its own goal instead.');
10777
+ console.log('--only limits it to those members. Same as atris member wake 5 or atris member wake a b.');
10778
+ console.log('Ctrl-C or atris member sleep --all ends it. Runs it started keep going.');
10779
+ return;
10780
+ }
10781
+ const root = process.cwd();
10782
+ const { createMemberUp, runMemberUp, parseMemberUpArgs, DEFAULT_MAX } = require('../lib/member-up');
10783
+ const parsed = parseMemberUpArgs(args);
10784
+ if (parsed.error) {
10785
+ console.error(parsed.error);
10786
+ process.exitCode = 1;
10787
+ return;
10788
+ }
10789
+ const { max, only } = parsed;
10790
+ const deps = {
10791
+ listMembers: () => memberUpTeam(root),
10792
+ pickTask: (name, taken) => memberUpPick(root, name, taken),
10793
+ spawn: (name, onExit) => memberUpSpawn(root, name, onExit),
10794
+ now: () => Date.now(),
10795
+ };
10796
+ if (parsed.dryRun) {
10797
+ const up = createMemberUp(deps, { max, only, dryRun: true });
10798
+ const started = up.check();
10799
+ if (!started.length && up.working() >= up.max) {
10800
+ console.log(`nothing to start: ${up.working()} already working, the limit is ${up.max}`);
10801
+ } else if (!started.length) {
10802
+ console.log('nothing to start: no free member has a task with a runnable check');
10803
+ }
10804
+ return started;
10805
+ }
10806
+ const pidFile = memberUpPidFile(root);
10807
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
10808
+ fs.writeFileSync(pidFile, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
10809
+ const loop = runMemberUp(deps, { max, only });
10810
+ const end = () => loop.stop();
10811
+ process.once('SIGINT', end);
10812
+ process.once('SIGTERM', end);
10813
+ const who = only ? ` for ${only.join(', ')}` : '';
10814
+ console.log(`team is up${who} (at most ${max || DEFAULT_MAX} at once). Ctrl-C to stop.`);
10815
+ await loop.done;
10816
+ process.removeListener('SIGINT', end);
10817
+ process.removeListener('SIGTERM', end);
10818
+ try {
10819
+ if (JSON.parse(fs.readFileSync(pidFile, 'utf8')).pid === process.pid) fs.unlinkSync(pidFile);
10820
+ } catch {}
10821
+ const working = loop.up.running();
10822
+ console.log(working.length ? `team is down; still finishing: ${working.join(', ')}` : 'team is down');
10823
+ }
10824
+
10825
+ // Ends a running `atris member up`. Returns true when one was told to stop.
10826
+ function stopMemberUp(root) {
10827
+ try {
10828
+ const { pid } = JSON.parse(fs.readFileSync(memberUpPidFile(root), 'utf8'));
10829
+ if (!Number.isInteger(pid) || pid === process.pid) return false;
10830
+ process.kill(pid, 'SIGTERM');
10831
+ return true;
10832
+ } catch {
10833
+ return false;
10834
+ }
10546
10835
  }
10547
10836
 
10548
10837
  function memberSleep(target, ...args) {
@@ -10568,6 +10857,7 @@ function memberSleep(target, ...args) {
10568
10857
  status: 'paused',
10569
10858
  paused_at: new Date().toISOString(),
10570
10859
  stop_reason: 'member-sleep',
10860
+ sleep_target: filterMember ? 'member' : 'all',
10571
10861
  next_action: `sleeping; wake with: atris member go ${owner || ''}`.trim(),
10572
10862
  updated_at: new Date().toISOString(),
10573
10863
  };
@@ -10577,9 +10867,11 @@ function memberSleep(target, ...args) {
10577
10867
  slept.push({ mission_id: m.id, member: owner || 'mission-lead', objective: m.objective });
10578
10868
  }
10579
10869
 
10870
+ const supervisorStopped = !filterMember && stopMemberUp(root);
10580
10871
  const payload = {
10581
10872
  ok: true,
10582
10873
  action: 'member_sleep',
10874
+ supervisor_stopped: supervisorStopped,
10583
10875
  target: filterMember || 'all',
10584
10876
  slept_count: slept.length,
10585
10877
  slept,
@@ -10595,6 +10887,7 @@ function memberSleep(target, ...args) {
10595
10887
  `All members are already resting.`,
10596
10888
  `Wake anytime: ${payload.wake_command}`,
10597
10889
  ];
10890
+ if (supervisorStopped) lines.unshift('Stopped member up; it starts nobody new.');
10598
10891
  printJsonOrText(payload, lines, asJson);
10599
10892
  return payload;
10600
10893
  }
@@ -10927,13 +11220,25 @@ function memberExport(...args) {
10927
11220
  return result;
10928
11221
  }
10929
11222
 
11223
+ function memberTidy(...args) {
11224
+ const { tidyMemberCopies, formatTidy } = require('../lib/member-tidy');
11225
+ const dirIndex = args.indexOf('--dir');
11226
+ const result = tidyMemberCopies({
11227
+ dryRun: args.includes('--dry-run'),
11228
+ worktreesDir: dirIndex >= 0 ? args[dirIndex + 1] : undefined,
11229
+ });
11230
+ console.log(formatTidy(result));
11231
+ if (result.aborted) process.exitCode = 1;
11232
+ return result;
11233
+ }
11234
+
10930
11235
  async function memberCommand(subcommand, ...args) {
10931
11236
  // Subcommands that take a member name as args[0] otherwise treat `--help` as
10932
11237
  // a name and error with "Member '--help' not found". `create`/`new` handle
10933
11238
  // help themselves (with subcommand-specific usage), leave those alone.
10934
11239
  const HELP_AWARE_SUBCOMMANDS = new Set([
10935
11240
  'create', 'new', 'run', 'install', 'result', 'dispatch', 'route', 'feed', 'stream',
10936
- 'card', 'cards', 'go', 'work', 'steer', 'sleep', 'rest', '1on1', 'interview',
11241
+ 'card', 'cards', 'go', 'work', 'up', 'steer', 'sleep', 'rest', '1on1', 'interview',
10937
11242
  'room', 'ui', 'day', 'standup', 'handoff', 'handoffs',
10938
11243
  ]);
10939
11244
  const explicitHelp = subcommand === 'help' || subcommand === '--help' || subcommand === '-h'
@@ -10971,9 +11276,17 @@ async function memberCommand(subcommand, ...args) {
10971
11276
  case 'tick':
10972
11277
  return memberTick(args[0], ...args.slice(1));
10973
11278
  case 'wake':
10974
- return memberWake(args[0], ...args.slice(1));
11279
+ return require('../lib/member-verbs').memberWakeVerb(args, {
11280
+ wake: memberWake,
11281
+ up: memberUp,
11282
+ });
10975
11283
  case 'run':
10976
- return memberRun(args[0], ...args.slice(1));
11284
+ return require('../lib/member-verbs').memberRunVerb(args, {
11285
+ root: process.cwd(),
11286
+ aliasOf: memberRuntimeKind,
11287
+ run: memberRun,
11288
+ dispatch: memberDispatch,
11289
+ });
10977
11290
  case 'result': {
10978
11291
  const code = require('../lib/member-result').memberResultCommand(args);
10979
11292
  if (code) process.exitCode = code;
@@ -10982,6 +11295,8 @@ async function memberCommand(subcommand, ...args) {
10982
11295
  case 'go':
10983
11296
  case 'work':
10984
11297
  return memberGo(...args);
11298
+ case 'up':
11299
+ return memberUp(...args);
10985
11300
  case 'steer':
10986
11301
  return memberSteerOrDispatch(...args);
10987
11302
  case 'sleep':
@@ -11006,6 +11321,8 @@ async function memberCommand(subcommand, ...args) {
11006
11321
  return memberFeed(...args);
11007
11322
  case 'board':
11008
11323
  return require('../lib/member-board').memberBoardCommand(...args);
11324
+ case 'stop':
11325
+ return require('../lib/member-board').memberStopCommand(args[0]);
11009
11326
  case 'card':
11010
11327
  case 'cards':
11011
11328
  return memberCard(args[0], ...args.slice(1));
@@ -11042,6 +11359,8 @@ async function memberCommand(subcommand, ...args) {
11042
11359
  return memberArchive(args[0]);
11043
11360
  case 'purge-archived':
11044
11361
  return memberPurgeArchived(...args);
11362
+ case 'tidy':
11363
+ return memberTidy(...args);
11045
11364
  case undefined:
11046
11365
  case '--json':
11047
11366
  if (fs.existsSync(path.join(process.cwd(), 'atris', 'team'))) {
@@ -11056,6 +11375,9 @@ async function memberCommand(subcommand, ...args) {
11056
11375
  }
11057
11376
  return memberSteerOrDispatch(subcommand, ...args);
11058
11377
  }
11378
+ console.log('run "<job>" pick the right member and run it: atris member run "fix the flaky login test"');
11379
+ console.log('wake <N|names> keep members working: atris member wake 5 or atris member wake navigator executor');
11380
+ console.log('sleep [name|--all] rest the team: atris member sleep --all');
11059
11381
  console.log('');
11060
11382
  console.log('Usage: atris member <subcommand|"<prompt>"> [name]');
11061
11383
  console.log('');
@@ -11072,6 +11394,7 @@ async function memberCommand(subcommand, ...args) {
11072
11394
  console.log(' install <slug> [--update] Install missing files, or refresh template skills');
11073
11395
  console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
11074
11396
  console.log(' list [--json] Show all team members');
11397
+ console.log(' tidy [--dry-run] Put away member copies of the repo whose work already landed');
11075
11398
  console.log(' export [--dry-run] Write each member as a subagent for Claude Code, Cursor, and Codex');
11076
11399
  console.log(' activate <name> Symlink member skills, show context and permissions');
11077
11400
  console.log(' upgrade <name> Convert flat file (name.md) to directory format');
@@ -11080,10 +11403,12 @@ async function memberCommand(subcommand, ...args) {
11080
11403
  console.log(' goal <name> "..." Create/update a member long-term goal');
11081
11404
  console.log(' goal-from-mission <name> Create/reuse a goal from MISSION.md and now.md');
11082
11405
  console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
11083
- console.log(' wake <name> Decide tick/wait/ask/stop, then run one bounded work step on the roster engine (--plan to only decide)');
11406
+ console.log(' wake <name> Decide tick/wait/ask/stop, then run one bounded work step on the roster engine (--plan to only decide; --keep keeps it working)');
11407
+ console.log(' run "<job>" Pick the best member for the job and run it (same as dispatch "<job>" --ignite)');
11084
11408
  console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
11085
11409
  console.log(' result <name> Newest run result: run record, receipt, artifacts, number [--since <iso>] [--json]');
11086
11410
  console.log(' dispatch "<idea>" Route an idea to the best specialist member and start work');
11411
+ console.log(' up [--max N] [--only a,b] Keep the team working: start free members on fitting tasks every minute (wake 5 / wake a b)');
11087
11412
  console.log(' feed [--member <n>] Show a plain-language linear feed of real work across the team');
11088
11413
  console.log(' card [name|--all] Show a member\'s roadmap, unlocked skills, live work, and recent wins');
11089
11414
  console.log(' loop <name|--all> Repeat wake on a bounded cadence with a no-overlap lease');
@@ -11144,7 +11469,9 @@ async function memberCommand(subcommand, ...args) {
11144
11469
  }
11145
11470
 
11146
11471
  module.exports = {
11472
+ memberRunWantsReferee,
11147
11473
  proposalPromptForGoal,
11474
+ memberGoFinishTask,
11148
11475
  appendMemberGoalLog,
11149
11476
  appendProjectLog,
11150
11477
  memberCommand,