atris 3.68.2 → 3.68.4

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.
@@ -8,7 +8,7 @@ const { resolveBackendRoot } = require('../utils/backend-root');
8
8
  const { apiRequestJson } = require('../utils/api');
9
9
  const { runAliveTick } = require('../lib/member-alive');
10
10
  const { memberRefusalReason } = require('../lib/member-type');
11
- const { defaultObjectiveRunner } = require('../lib/default-runner');
11
+ const { callerRuntime, defaultObjectiveRunner } = require('../lib/default-runner');
12
12
  const { readJson, writeJson } = require('../lib/json-file');
13
13
  const { sleepSync } = require('../lib/sleep-sync');
14
14
  const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
@@ -383,7 +383,10 @@ function loadMemberGoals(name, paths = memberPaths(name)) {
383
383
 
384
384
  function activeGoal(goalsState, goalId = '') {
385
385
  if (goalId) return goalsState.goals.find((goal) => goal.id === goalId) || null;
386
- return goalsState.goals.find((goal) => goal.status === 'active') || goalsState.goals[0] || null;
386
+ for (let i = (goalsState.goals || []).length - 1; i >= 0; i -= 1) {
387
+ if (goalsState.goals[i]?.status === 'active') return goalsState.goals[i];
388
+ }
389
+ return goalsState.goals[0] || null;
387
390
  }
388
391
 
389
392
  function allExperiments(state) {
@@ -490,9 +493,18 @@ function missionPurpose(paths) {
490
493
  const MEMBER_RUN_RUNNABLE_STATUSES = new Set(['planning', 'running', 'ready']);
491
494
  const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped', 'failed']);
492
495
 
493
- function memberRunMissionMap() {
496
+ function memberRunMissionMap(root = process.cwd()) {
494
497
  try {
495
- return require('./mission').loadMissionMap(process.cwd());
498
+ const missionMod = require('./mission');
499
+ const map = new Map(missionMod.loadMissionMap(root));
500
+ for (const rolled of missionMod.listWorktreeRollupMissions(root)) {
501
+ if (!rolled?.id) continue;
502
+ const existing = map.get(rolled.id);
503
+ if (!existing || missionTimeMs(rolled) >= missionTimeMs(existing)) {
504
+ map.set(rolled.id, rolled);
505
+ }
506
+ }
507
+ return map;
496
508
  } catch {
497
509
  return new Map();
498
510
  }
@@ -627,18 +639,38 @@ function memberLastActivity(name, paths = memberPaths(name), root = process.cwd(
627
639
  };
628
640
  }
629
641
 
630
- function memberMissionSummary(name, missionMap = memberRunMissionMap()) {
642
+ function memberMissionSummary(name, missionMap = memberRunMissionMap(), root = process.cwd()) {
631
643
  const latest = memberLatestMission(name, missionMap);
632
644
  const live = memberLiveMission(name, missionMap);
633
645
  const lastTickMs = timestampMs(latest?.last_tick_at);
646
+ let driver = null;
647
+ if (latest) {
648
+ try {
649
+ const { missionDriverHealth } = require('./mission');
650
+ const missionRoot = latest.worktree_root || latest.worktree?.path || root;
651
+ driver = missionDriverHealth(latest, missionRoot);
652
+ } catch {
653
+ driver = null;
654
+ }
655
+ }
656
+ const worktreeRoot = latest?.worktree_root || latest?.worktree?.path || null;
657
+ const worktreeBranch = latest?.worktree_branch || latest?.worktree?.branch || null;
634
658
  return {
635
659
  latest,
636
660
  live,
637
661
  name: missionShortName(latest),
662
+ objective: latest?.objective || null,
638
663
  state: latest?.status || '-',
664
+ runner: latest?.runner || null,
665
+ model: latest?.model || null,
666
+ worktree_root: worktreeRoot,
667
+ worktree_branch: worktreeBranch,
668
+ task_ref: latest?.task_ref || latest?.xp_task?.ref || null,
669
+ current_task_id: latest?.current_task_id || latest?.task_ids?.[0] || null,
639
670
  last_tick_at: latest?.last_tick_at || null,
640
671
  last_tick_age: ageLabelFromMs(lastTickMs),
641
- live_mission: Boolean(live),
672
+ live_mission: Boolean(live) || Boolean(driver?.alive),
673
+ driver,
642
674
  };
643
675
  }
644
676
 
@@ -720,6 +752,7 @@ const MEMBER_RUN_START_VALUE_FLAGS = [
720
752
  '--visible-goal-objective',
721
753
  '--max-idle-ticks',
722
754
  '--slots',
755
+ '--referee-engine',
723
756
  '--lane',
724
757
  '--cadence',
725
758
  '--verify',
@@ -735,11 +768,14 @@ const MEMBER_RUN_START_VALUE_FLAGS = [
735
768
  '--outcome',
736
769
  '--truth',
737
770
  '--proof',
771
+ '--base',
738
772
  ];
739
773
 
740
774
  const MEMBER_RUN_START_BOOLEAN_FLAGS = [
741
775
  '--json',
742
776
  '--no-verify',
777
+ '--referee',
778
+ '--no-referee',
743
779
  '--worktree',
744
780
  '--shared-checkout',
745
781
  '--no-worktree',
@@ -773,6 +809,10 @@ const MEMBER_RUN_START_BOOLEAN_FLAGS = [
773
809
  '--dry-run',
774
810
  '--land',
775
811
  '--cloud',
812
+ '--ignite',
813
+ '--wait',
814
+ '--sync',
815
+ '--no-driver',
776
816
  ];
777
817
 
778
818
  function memberRunMissionWordIndices(args = []) {
@@ -900,7 +940,23 @@ function memberRunRunner(owner, args = [], cwd = process.cwd()) {
900
940
  return memberRosterEngine(owner, cwd) ? 'auto' : fallback;
901
941
  }
902
942
 
943
+ function inferMemberRunVerifier(missionText, args = [], cwd = process.cwd()) {
944
+ if (hasFlag(args, '--no-verify')) return '';
945
+ const explicit = readFlag(args, '--verify', '');
946
+ if (explicit) return explicit;
947
+ const text = String(missionText || '').trim();
948
+ if (!text) return '';
949
+ const cmdMatch = text.match(/\b(npm\s+run\s+type-check|node\s+--test\s+tests?\/[A-Za-z0-9_./-]+)\b/);
950
+ if (cmdMatch) return cmdMatch[1].replace(/\s+/g, ' ');
951
+ const testFileMatch = text.match(/\b(tests?\/[A-Za-z0-9_./-]+\.test\.m?js)\b/);
952
+ if (testFileMatch && fs.existsSync(path.join(cwd, testFileMatch[1]))) {
953
+ return `node --test ${testFileMatch[1]}`;
954
+ }
955
+ return '';
956
+ }
957
+
903
958
  function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cwd()) {
959
+ const resolvedVerifier = inferMemberRunVerifier(missionText, args, cwd);
904
960
  const startArgs = [
905
961
  'mission',
906
962
  'start',
@@ -913,7 +969,9 @@ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cw
913
969
  readFlag(args, '--lane', 'workspace'),
914
970
  ];
915
971
  pushFlagValue(startArgs, args, '--cadence');
916
- pushFlagValue(startArgs, args, '--verify');
972
+ if (resolvedVerifier) {
973
+ startArgs.push('--verify', String(resolvedVerifier));
974
+ }
917
975
  pushFlagValue(startArgs, args, '--stop');
918
976
  pushFlagValue(startArgs, args, '--model');
919
977
  pushFlagValue(startArgs, args, '--max-wall');
@@ -921,8 +979,9 @@ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cw
921
979
  pushFlagValue(startArgs, args, '--hours');
922
980
  pushFlagValue(startArgs, args, '--base');
923
981
  // Member runs choose and drive work immediately, so an absent check must not
924
- // leave autonomous callers with a mission that never ran.
925
- if (hasFlag(args, '--no-verify') || !readFlag(args, '--verify', '')) {
982
+ // leave autonomous callers with a mission that never ran. A check inferred
983
+ // from the mission text counts as named.
984
+ if (hasFlag(args, '--no-verify') || !resolvedVerifier) {
926
985
  startArgs.push('--no-verify');
927
986
  }
928
987
  pushFlagWhenPresent(startArgs, args, '--always-on');
@@ -992,16 +1051,96 @@ function memberRunConfiguredVerifier(paths) {
992
1051
  return typeof check === 'string' ? check.trim() : '';
993
1052
  }
994
1053
 
995
- function printMemberRunResult(result, asJson) {
996
- if (asJson) {
1054
+ function printMemberRunResult(result, asJson, { quiet = false } = {}) {
1055
+ if (quiet) {
1056
+ // A caller (member wake) prints this result inside its own report.
1057
+ } else if (asJson) {
997
1058
  console.log(JSON.stringify(result));
998
1059
  } else if (result.started) {
999
1060
  console.log(`started ${result.mission_id} (${result.state})`);
1000
1061
  }
1062
+ if (!quiet && !asJson && result.ask) console.error(result.ask);
1001
1063
  if (result.ok === false || (!result.started && result.ok !== true)) {
1002
- if (!result.errorReported) console.error(result.error);
1064
+ if (!quiet && !result.errorReported && !result.ask) console.error(result.error);
1003
1065
  process.exitCode = 1;
1004
1066
  }
1067
+ return result;
1068
+ }
1069
+
1070
+ // Runners that hand the mission to the agent session that called Atris. From
1071
+ // a plain terminal nothing is on the other end of them.
1072
+ const MEMBER_RUN_CALLER_SESSION_RUNNERS = new Set(['codex_goal', 'caller_session', 'current_agent']);
1073
+
1074
+ function memberRunUsesCallerSession(runner) {
1075
+ return MEMBER_RUN_CALLER_SESSION_RUNNERS.has(String(runner || '').trim().toLowerCase());
1076
+ }
1077
+
1078
+ // A mission whose runner waits on a caller session cannot do anything when
1079
+ // no such session started this run. Swap in the roster engine for this run
1080
+ // ("auto" resolves the owner's roster pick per tick) instead of waiting on a
1081
+ // native goal nothing will start. An explicit --runner or --engine on this
1082
+ // command still wins, and a real codex caller keeps its native goal.
1083
+ function memberRunRunnerSwap(mission, args, caller) {
1084
+ if (!memberRunUsesCallerSession(mission?.runner)) return null;
1085
+ if (caller === 'codex') return null;
1086
+ if (readFlag(args, '--runner', '') || readFlag(args, '--engine', '')) return null;
1087
+ return {
1088
+ from: String(mission.runner),
1089
+ to: 'auto',
1090
+ reason: `no ${String(mission.runner) === 'codex_goal' ? 'codex' : 'agent'} session started this run, so the member's roster engine does the work`,
1091
+ };
1092
+ }
1093
+
1094
+ function memberRunTickDurationMs(tick) {
1095
+ const start = Date.parse(tick?.started_at || '');
1096
+ const end = Date.parse(tick?.finished_at || '');
1097
+ return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : null;
1098
+ }
1099
+
1100
+ // One plain record of the work step this run executed (or why none ran), so
1101
+ // a reader never has to dig through mission internals to learn what happened.
1102
+ function memberRunWorkStep(payload, finalMission) {
1103
+ const ticks = Array.isArray(payload?.ticks) ? payload.ticks : [];
1104
+ const ran = [...ticks].reverse().find((tick) => tick && tick.ran === true) || null;
1105
+ const last = ran || ticks[ticks.length - 1] || null;
1106
+ const worker = last?.claude || last?.atris2 || null;
1107
+ return {
1108
+ ran: Boolean(ran),
1109
+ ticks_ran: Number(payload?.ran_ticks) || 0,
1110
+ engine: last?.engine_id || finalMission?.runner || null,
1111
+ model: last?.model || worker?.model || finalMission?.model || null,
1112
+ duration_ms: last ? memberRunTickDurationMs(last) : null,
1113
+ status: last?.status || null,
1114
+ reason: last?.reason || null,
1115
+ summary: worker?.summary ? String(worker.summary).slice(0, 400) : null,
1116
+ receipt_path: payload?.summary_receipt || finalMission?.receipt_path || null,
1117
+ };
1118
+ }
1119
+
1120
+ // The protected-surface guard keeps the staged work and pauses for a person.
1121
+ // Say so in one line, with the command that shows the work.
1122
+ function memberRunReviewAsk(name, payload, mission, cwd) {
1123
+ const ticks = Array.isArray(payload?.ticks) ? payload.ticks : [];
1124
+ const guarded = ticks.find((tick) => tick?.protected_lane_guard && tick.protected_lane_guard.allowed === false);
1125
+ const guard = guarded?.protected_lane_guard || null;
1126
+ if (!guard) return null;
1127
+ const surfaces = Array.isArray(guard.surfaces) && guard.surfaces.length ? guard.surfaces.join(', ') : 'a protected surface';
1128
+ if (guard.unreadable) {
1129
+ return {
1130
+ needs_review: true,
1131
+ ask: `${name} paused: Atris could not read the mission diff; fix that, then rerun.`,
1132
+ next_command: `atris mission tick ${mission.id} --verify`,
1133
+ review_path: cwd,
1134
+ };
1135
+ }
1136
+ const where = JSON.stringify(cwd);
1137
+ return {
1138
+ needs_review: true,
1139
+ ask: `${name} staged a change that touches ${surfaces}; it is kept in ${cwd} for you to review, land, or discard.`,
1140
+ next_command: `git -C ${where} status --short && git -C ${where} diff HEAD`,
1141
+ resume_command: `atris mission run ${mission.id}`,
1142
+ review_path: cwd,
1143
+ };
1005
1144
  }
1006
1145
 
1007
1146
  // atris member ping <name> "<message>": talk to a busy member mid-run.
@@ -1087,10 +1226,15 @@ function memberPing(name, ...args) {
1087
1226
  }
1088
1227
 
1089
1228
  function memberRun(name, ...args) {
1229
+ return memberRunWith(name, args);
1230
+ }
1231
+
1232
+ function memberRunWith(name, args, { quiet = false } = {}) {
1090
1233
  if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
1091
1234
  console.log('usage: atris member run <name> ["mission text"] [--verify "<cmd>"] [--minutes N|--hours N] [--json]');
1092
1235
  console.log('new mission: atris member run growth "improve onboarding proof" --verify "npm test" --minutes 30 --json');
1093
1236
  console.log('choose work: atris member run growth --verify "npm test" --industry logistics --minutes 30 --json');
1237
+ console.log('subagent shell: atris member run growth "improve onboarding proof" --engine claude --ignite --json');
1094
1238
  console.log('existing mission: atris member run growth --mission <mission-id> --max-ticks 1 --json');
1095
1239
  console.log('meaning: if mission text is omitted, the member chooses one useful bounded task from atris state.');
1096
1240
  console.log('truth: useful work must improve revenue, reliability, speed, security, clarity, or trust, then show plain proof.');
@@ -1099,6 +1243,12 @@ function memberRun(name, ...args) {
1099
1243
  return;
1100
1244
  }
1101
1245
 
1246
+ // --ignite (or --wait, --sync) runs the member as a subagent shell: it works
1247
+ // on the named engine until the check passes, then the mission completes.
1248
+ const ignite = hasFlag(args, '--ignite') || hasFlag(args, '--wait') || hasFlag(args, '--sync');
1249
+ if (ignite && readFlag(args, '--engine', '') && !readFlag(args, '--runner', '')) {
1250
+ args = [...args, '--runner', readFlag(args, '--engine', '')];
1251
+ }
1102
1252
  const asJson = hasFlag(args, '--json');
1103
1253
  const paths = requireRunnableMember(name);
1104
1254
  // Parking only hides a member from the team views; a run by name still goes.
@@ -1118,9 +1268,8 @@ function memberRun(name, ...args) {
1118
1268
  if (missionText && !hasMissionOverride) {
1119
1269
  const start = startMemberRunMission(name, missionText, requestedVerifier ? [...args, '--verify', requestedVerifier] : args);
1120
1270
  if (start.status !== 0 || !start.payload?.mission) {
1121
- printMemberRunResult({ started: false, state: null, mission_id: start.payload?.mission?.id || null,
1122
- error: start.payload?.detail || start.payload?.error || start.stderr || 'mission creation failed' }, asJson);
1123
- return;
1271
+ return printMemberRunResult({ started: false, state: null, mission_id: start.payload?.mission?.id || null,
1272
+ error: start.payload?.detail || start.payload?.error || start.stderr || 'mission creation failed' }, asJson, { quiet });
1124
1273
  }
1125
1274
  mission = start.payload.mission;
1126
1275
  startedNewMission = true;
@@ -1131,9 +1280,8 @@ function memberRun(name, ...args) {
1131
1280
  if (!missionId) {
1132
1281
  const start = startMemberRunMission(name, memberRunAutoMissionText(name, args), requestedVerifier ? [...args, '--verify', requestedVerifier] : args);
1133
1282
  if (start.status !== 0 || !start.payload?.mission) {
1134
- printMemberRunResult({ started: false, state: null, mission_id: start.payload?.mission?.id || null,
1135
- error: start.payload?.detail || start.payload?.error || start.stderr || 'mission creation failed' }, asJson);
1136
- return;
1283
+ return printMemberRunResult({ started: false, state: null, mission_id: start.payload?.mission?.id || null,
1284
+ error: start.payload?.detail || start.payload?.error || start.stderr || 'mission creation failed' }, asJson, { quiet });
1137
1285
  }
1138
1286
  mission = start.payload.mission;
1139
1287
  startedNewMission = true;
@@ -1142,8 +1290,7 @@ function memberRun(name, ...args) {
1142
1290
  } else {
1143
1291
  mission = require('./mission').resolveMission(missionId);
1144
1292
  if (!mission) {
1145
- printMemberRunResult({ started: false, state: null, mission_id: missionId, error: `mission ${missionId} was not found` }, asJson);
1146
- return;
1293
+ return printMemberRunResult({ started: false, state: null, mission_id: missionId, error: `mission ${missionId} was not found` }, asJson, { quiet });
1147
1294
  }
1148
1295
  }
1149
1296
  }
@@ -1158,9 +1305,8 @@ function memberRun(name, ...args) {
1158
1305
  if (verifier && verifier !== mission.verifier) {
1159
1306
  const changed = memberRunCli(['mission', 'set-verifier', mission.id, verifier, '--json'], cwd);
1160
1307
  if (changed.status !== 0) {
1161
- printMemberRunResult({ started: false, state: mission.status, mission_id: mission.id, mission,
1162
- error: changed.payload?.detail || changed.payload?.error || changed.stderr || 'could not set mission check' }, asJson);
1163
- return;
1308
+ return printMemberRunResult({ started: false, state: mission.status, mission_id: mission.id, mission,
1309
+ error: changed.payload?.detail || changed.payload?.error || changed.stderr || 'could not set mission check' }, asJson, { quiet });
1164
1310
  }
1165
1311
  mission.verifier = verifier;
1166
1312
  }
@@ -1169,22 +1315,54 @@ function memberRun(name, ...args) {
1169
1315
  const executionArgs = args.filter((_, index) => !missionWordIndices.has(index));
1170
1316
  const runArgs = stripKnownFlags(executionArgs,
1171
1317
  ['--mission', '--mission-id', '--minutes', '--hours', '--industry', '--domain', '--value', '--outcome', '--truth', '--proof', '--base'],
1172
- ['--worktree', '--shared-checkout', '--no-worktree', '--choose-work', '--auto']);
1318
+ ['--worktree', '--shared-checkout', '--no-worktree', '--choose-work', '--auto', '--ignite', '--wait', '--sync']);
1319
+ if (ignite && !hasFlag(runArgs, '--complete-on-pass')) runArgs.push('--complete-on-pass');
1173
1320
  const budgetSeconds = memberRunBudgetSeconds(args);
1174
1321
  if (!readFlag(runArgs, '--max-ticks', '')) {
1175
1322
  runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
1176
1323
  }
1177
1324
  if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', String(budgetSeconds || 900));
1178
- const run = memberRunCli(['mission', 'run', mission.id, ...runArgs], cwd, { live: !asJson, missionId: mission.id });
1325
+ const caller = callerRuntime(args);
1326
+ const runnerSwap = memberRunRunnerSwap(mission, args, caller);
1327
+ if (runnerSwap) runArgs.push('--engine', runnerSwap.to);
1328
+ const run = memberRunCli(['mission', 'run', mission.id, ...runArgs], cwd, { live: !asJson && !quiet, missionId: mission.id });
1179
1329
  const payload = run.payload || {};
1180
1330
  const finalMission = payload.mission || require('./mission').resolveMission(mission.id, cwd) || mission;
1181
1331
  const started = run.status === 0 && payload.ok !== false && Number(payload.ran_ticks) > 0;
1332
+ // Inside a real codex session the native goal is the work step: the caller
1333
+ // starts it with the action below, so waiting on it is not a failure.
1334
+ const waitingOnNativeGoal = caller === 'codex' && payload.requires_native_goal_start === true
1335
+ && memberRunUsesCallerSession(finalMission.runner);
1182
1336
  const noWorkStepExpected = hasFlag(runArgs, '--detach') || hasFlag(runArgs, '--dry-run');
1183
- const ok = run.status === 0 && payload.ok !== false && (started || noWorkStepExpected);
1184
- printMemberRunResult({ ...payload, ok, started, state: finalMission.status, mission_id: mission.id,
1337
+ const ok = (run.status === 0 && payload.ok !== false && (started || noWorkStepExpected)) || waitingOnNativeGoal;
1338
+ const reviewAsk = memberRunReviewAsk(name, payload, finalMission, cwd);
1339
+ const result = { ...payload, ok, started, state: finalMission.status, mission_id: mission.id,
1185
1340
  mission: finalMission, node_modules_linked: nodeModulesLinked, node_modules_source: nodeModulesSource,
1186
- errorReported: run.errorReported,
1187
- ...(!ok ? { error: payload.detail || payload.error || payload.reason || run.stderr || 'mission did not run a work step' } : {}) }, asJson);
1341
+ started_mission: startedNewMission ? mission : null, worktree: mission.worktree || null,
1342
+ caller_runtime: caller,
1343
+ runner_swap: runnerSwap,
1344
+ work_step: memberRunWorkStep(payload, finalMission),
1345
+ errorReported: run.errorReported };
1346
+ // The goal-controller view describes whichever mission holds the codex goal
1347
+ // slot, often an older one. On a run that is not a native goal it reads as
1348
+ // "waiting on create_goal" after this run already worked, so leave it out.
1349
+ if (runnerSwap || !memberRunUsesCallerSession(finalMission.runner)) {
1350
+ delete result.codex_goal_state;
1351
+ }
1352
+ if (waitingOnNativeGoal) {
1353
+ result.waiting_on = 'native_goal_start';
1354
+ result.next_command = payload.next_action || payload.next_command || null;
1355
+ }
1356
+ if (reviewAsk) Object.assign(result, reviewAsk);
1357
+ if (!reviewAsk && payload.reason === 'human_asks_open' && Array.isArray(payload.human_asks) && payload.human_asks.length) {
1358
+ result.ask = `${name} is waiting on you: ${payload.human_asks[0]}`;
1359
+ result.next_command = payload.next_command || 'atris decide';
1360
+ }
1361
+ if (!ok) {
1362
+ result.error = result.ask || payload.detail || payload.error || payload.reason || payload.next_action
1363
+ || run.stderr || 'mission did not run a work step';
1364
+ }
1365
+ return printMemberRunResult(result, asJson, { quiet });
1188
1366
  }
1189
1367
 
1190
1368
  function loadTeamScoreEvidence(scoreJsonPath) {
@@ -1385,7 +1563,23 @@ function taskHasReviewProof(task) {
1385
1563
  return (task?.events || []).some((event) => event?.payload?.proof || event?.payload?.review?.proof);
1386
1564
  }
1387
1565
 
1566
+ function taskIsTaggedNeedsHuman(task) {
1567
+ if (!task || typeof task !== 'object') return false;
1568
+ if (task?.metadata?.needs_human === true || task?.metadata?.human_only === true) return true;
1569
+ const tags = [
1570
+ task.tag,
1571
+ ...(Array.isArray(task.tags) ? task.tags : []),
1572
+ task.metadata?.tag,
1573
+ ...(Array.isArray(task.metadata?.tags) ? task.metadata.tags : []),
1574
+ ]
1575
+ .filter(Boolean)
1576
+ .flatMap((t) => String(t).split(/[,;\s]+/))
1577
+ .map(lowerCompact);
1578
+ return tags.some((t) => t === 'needs-human' || t === 'needs_human' || t === 'human-only' || t === 'human_only');
1579
+ }
1580
+
1388
1581
  function taskCandidateFromSource(task, source, sourcePath = '') {
1582
+ if (taskIsTaggedNeedsHuman(task)) return null;
1389
1583
  const ref = taskRef(task);
1390
1584
  if (!ref) return null;
1391
1585
  const status = lowerCompact(task.status || task.state || 'open');
@@ -3295,6 +3489,7 @@ function collectAutoImproverLogSignals(root) {
3295
3489
  }
3296
3490
 
3297
3491
  function autoImproverTaskWaitingForHuman(task, status) {
3492
+ if (taskIsTaggedNeedsHuman(task)) return true;
3298
3493
  if (status !== 'review') return false;
3299
3494
  const metadata = task?.metadata || {};
3300
3495
  const review = task?.review || {};
@@ -3826,6 +4021,9 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
3826
4021
  });
3827
4022
  }
3828
4023
 
4024
+ const purpose = missionPurpose(paths);
4025
+ const currentGoal = activeGoal(loadMemberGoals(name, paths));
4026
+
3829
4027
  return {
3830
4028
  ok: true,
3831
4029
  action: 'wake',
@@ -3838,6 +4036,13 @@ async function runAutoImproverWake(name, paths, { execute = false, confirmed = f
3838
4036
  needs_user: false,
3839
4037
  ask: null,
3840
4038
  next_command: nextCommand,
4039
+ mission: wakeMissionSummary(purpose),
4040
+ active_goal: currentGoal ? {
4041
+ id: currentGoal.id,
4042
+ title: currentGoal.title,
4043
+ source: currentGoal.source || null,
4044
+ mission_id: currentGoal.mission_id || null,
4045
+ } : null,
3841
4046
  auto_improver: payload,
3842
4047
  receipt_path: finalWrite.receiptPath,
3843
4048
  latest_path: finalWrite.latestPath,
@@ -3957,8 +4162,10 @@ function appendMemberGoalLog(memberDir, name, title, fields = {}) {
3957
4162
  `## ${stamp} · ${title}`,
3958
4163
  `- team: ${name}`,
3959
4164
  ...Object.entries(fields)
3960
- .filter(([, value]) => value !== undefined && value !== null && value !== '')
3961
- .map(([key, value]) => `- ${key}: ${String(value).replace(/\n/g, ' ')}`),
4165
+ .filter(([, value]) => value !== undefined && value !== null)
4166
+ .map(([key, value]) => [key, String(value).replace(/\n/g, ' ').trim()])
4167
+ .filter(([, text]) => text !== '')
4168
+ .map(([key, text]) => `- ${key}: ${text}`),
3962
4169
  '',
3963
4170
  ];
3964
4171
  fs.appendFileSync(logPath, rows.join('\n'), 'utf8');
@@ -3975,8 +4182,10 @@ function appendProjectLog(title, fields = {}) {
3975
4182
  const rows = [
3976
4183
  `## ${stamp} · ${title}`,
3977
4184
  ...Object.entries(fields)
3978
- .filter(([, value]) => value !== undefined && value !== null && value !== '')
3979
- .map(([key, value]) => `- ${key}: ${String(value).replace(/\n/g, ' ')}`),
4185
+ .filter(([, value]) => value !== undefined && value !== null)
4186
+ .map(([key, value]) => [key, String(value).replace(/\n/g, ' ').trim()])
4187
+ .filter(([, text]) => text !== '')
4188
+ .map(([key, text]) => `- ${key}: ${text}`),
3980
4189
  '',
3981
4190
  ];
3982
4191
  fs.appendFileSync(logPath, rows.join('\n'), 'utf8');
@@ -4834,6 +5043,11 @@ function memberGoal(name, ...args) {
4834
5043
  experiments: [],
4835
5044
  history: [],
4836
5045
  };
5046
+ for (const prev of state.goals) {
5047
+ if (prev && prev !== existing && prev.status === 'active') {
5048
+ prev.status = 'superseded';
5049
+ }
5050
+ }
4837
5051
  goal.status = 'active';
4838
5052
  goal.cadence = cadence || goal.cadence || 'manual';
4839
5053
  if (why) goal.why = why;
@@ -7577,9 +7791,15 @@ function readMemberScope(name, paths) {
7577
7791
  // and must not gate the next wake, otherwise the loop deadlocks on its own log writes.
7578
7792
  function isAutoGeneratedArtifact(filePath, name) {
7579
7793
  const memberLogs = `atris/team/${name}/logs/`;
7794
+ const memberSkills = `atris/team/${name}/skills/`;
7580
7795
  if (filePath.startsWith(memberLogs)) return true;
7796
+ if (filePath.startsWith(memberSkills)) return true;
7581
7797
  if (filePath === `atris/team/${name}/goals.json`) return true;
7798
+ if (filePath === `atris/team/${name}/goals.md`) return true;
7582
7799
  if (filePath === `atris/team/${name}/now.md`) return true;
7800
+ if (filePath === `atris/team/${name}/MEMBER.md`) return true;
7801
+ if (filePath === `atris/team/${name}/MISSION.md`) return true;
7802
+ if (filePath === `atris/team/${name}/SOUL.md`) return true;
7583
7803
  return false;
7584
7804
  }
7585
7805
 
@@ -8292,7 +8512,91 @@ async function memberWake(name, ...args) {
8292
8512
  // switch store unreadable: fail open and continue wake
8293
8513
  }
8294
8514
  const result = await runMemberWake(name, { execute, confirmed, force, domainInput });
8295
- printJsonOrText(result, wakeBootLines(name, result), asJson);
8515
+ const work = wakeWorkStepPlan(name, args, result, { execute });
8516
+ if (!work.run) {
8517
+ if (work.reason) result.work_step = { ran: false, skipped: true, reason: work.reason };
8518
+ printJsonOrText(result, wakeBootLines(name, result), asJson);
8519
+ return result;
8520
+ }
8521
+ // Wake then work: one bounded member run on the roster engine, so waking a
8522
+ // member from a terminal (or cron) does something instead of only deciding.
8523
+ if (!asJson) printJsonOrText(result, wakeBootLines(name, result), false);
8524
+ const runArgs = [...(work.objective ? [work.objective] : []), ...work.flags, ...(asJson ? ['--json'] : [])];
8525
+ const run = memberRunWith(name, runArgs, { quiet: asJson });
8526
+ result.work_step = wakeWorkStepSummary(run, work);
8527
+ if (asJson) printJsonOrText(result, [], true);
8528
+ else for (const line of wakeWorkStepLines(result.work_step)) console.log(line);
8529
+ return result;
8530
+ }
8531
+
8532
+ // Wake decisions that leave a member free to work now. Everything else
8533
+ // (ask, wait on a review or dirty files, stop for another reason) keeps the
8534
+ // member's own judgment and only reports it.
8535
+ const WAKE_WORK_DECISIONS = new Set(['tick', 'set_objective', 'create_task']);
8536
+
8537
+ // Whether this wake also runs one bounded work step, and on what. A wake from
8538
+ // a plain terminal (or cron) runs one when the roster names the member's
8539
+ // engine. --plan or --dry-run only plans; --run forces the step; the legacy
8540
+ // --execute path keeps proposing experiments; a codex or agent session around
8541
+ // the CLI does the work itself, so it only plans.
8542
+ function wakeWorkStepPlan(name, args, result, { execute = false } = {}) {
8543
+ if (!name || !result || result.ok === false || result.skipped) return { run: false };
8544
+ if (hasFlag(args, '--plan') || hasFlag(args, '--dry-run')) return { run: false };
8545
+ const forced = hasFlag(args, '--run') || hasFlag(args, '--work');
8546
+ if (execute && !forced) return { run: false };
8547
+ const caller = callerRuntime(args);
8548
+ if (!forced && caller !== 'terminal') return { run: false, reason: `${caller} session does the work itself` };
8549
+ const decision = String(result.decision || '');
8550
+ const freeToWork = WAKE_WORK_DECISIONS.has(decision)
8551
+ || (decision === 'stop' && result.reason === 'no_active_goal');
8552
+ if (result.needs_user || !freeToWork) {
8553
+ return { run: false, reason: `wake decided ${decision || 'nothing'} (${result.reason || 'no reason'})` };
8554
+ }
8555
+ if (!forced) {
8556
+ const { memberRosterEngine } = require('../lib/member-engine');
8557
+ if (!memberRosterEngine(name)) return { run: false, reason: 'no roster engine for this member; add a build pick to ROSTER.md or pass --run' };
8558
+ }
8559
+ const objective = decision === 'tick'
8560
+ ? String(result.active_goal?.title || '').trim()
8561
+ : String(result.autonomous_problem?.problem || result.autonomous_problem?.title || '').trim();
8562
+ const flags = [];
8563
+ for (const flag of ['--minutes', '--hours', '--verify']) {
8564
+ const value = readFlag(args, flag, '');
8565
+ if (value) flags.push(flag, value);
8566
+ }
8567
+ if (!readFlag(args, '--minutes', '') && !readFlag(args, '--hours', '')) flags.push('--minutes', '15');
8568
+ if (!readFlag(args, '--verify', '')) flags.push('--no-verify');
8569
+ return { run: true, objective, flags, caller };
8570
+ }
8571
+
8572
+ function wakeWorkStepSummary(run, work) {
8573
+ const step = run?.work_step || {};
8574
+ return {
8575
+ ran: step.ran === true,
8576
+ ok: run?.ok === true,
8577
+ mission_id: run?.mission_id || null,
8578
+ objective: work.objective || run?.mission?.objective || null,
8579
+ engine: step.engine || null,
8580
+ model: step.model || null,
8581
+ duration_ms: step.duration_ms == null ? null : step.duration_ms,
8582
+ summary: step.summary || null,
8583
+ receipt_path: step.receipt_path || null,
8584
+ review_path: run?.mission?.worktree?.path || null,
8585
+ ...(run?.needs_review ? { needs_review: true, ask: run.ask, next_command: run.next_command } : {}),
8586
+ ...(run?.ok === false && !run?.needs_review ? { error: run.error || 'the work step did not run' } : {}),
8587
+ };
8588
+ }
8589
+
8590
+ function wakeWorkStepLines(step) {
8591
+ if (!step) return [];
8592
+ if (step.needs_review) return ['', ` ${step.ask}`, ` ${step.next_command}`];
8593
+ if (!step.ran) return ['', ` work step did not run: ${step.error || step.reason || 'unknown'}`];
8594
+ const seconds = step.duration_ms == null ? '' : ` in ${Math.round(step.duration_ms / 1000)}s`;
8595
+ return [
8596
+ '',
8597
+ ` worked on ${step.engine}${step.model ? ` ${step.model}` : ''}${seconds}`,
8598
+ ...(step.receipt_path ? [` receipt \u00b7 ${step.receipt_path}`] : []),
8599
+ ];
8296
8600
  }
8297
8601
 
8298
8602
  function memberAlive(name, ...args) {
@@ -9015,14 +9319,14 @@ function memberBlock(name, experimentId, ...args) {
9015
9319
 
9016
9320
  function collectFleetMemberStatus(root = process.cwd()) {
9017
9321
  const teamDir = path.join(root, 'atris', 'team');
9018
- const missionMap = memberRunMissionMap();
9322
+ const missionMap = memberRunMissionMap(root);
9019
9323
  return findAllMembers(teamDir)
9020
9324
  .filter(isFleetMember)
9021
9325
  .map((member) => {
9022
9326
  const slug = memberSlugFromRecord(member);
9023
9327
  const paths = memberPaths(slug);
9024
9328
  const goalPlane = memberGoalPlaneStatus(slug, paths);
9025
- const mission = memberMissionSummary(slug, missionMap);
9329
+ const mission = memberMissionSummary(slug, missionMap, root);
9026
9330
  const activity = memberLastActivity(slug, paths, root);
9027
9331
  const verdict = memberVerdict({ mission, activity });
9028
9332
  return {
@@ -9033,10 +9337,18 @@ function collectFleetMemberStatus(root = process.cwd()) {
9033
9337
  mission: mission.latest ? {
9034
9338
  id: mission.latest.id || null,
9035
9339
  name: mission.name,
9340
+ objective: mission.objective,
9036
9341
  state: mission.state,
9342
+ runner: mission.runner,
9343
+ model: mission.model,
9344
+ worktree_root: mission.worktree_root,
9345
+ worktree_branch: mission.worktree_branch,
9346
+ task_ref: mission.task_ref,
9347
+ current_task_id: mission.current_task_id,
9037
9348
  last_tick_at: mission.last_tick_at,
9038
9349
  last_tick_age: mission.last_tick_age,
9039
9350
  live: mission.live_mission,
9351
+ driver: mission.driver,
9040
9352
  } : null,
9041
9353
  last_activity: activity,
9042
9354
  verdict,
@@ -9148,10 +9460,18 @@ function memberStatus(name, ...args) {
9148
9460
  mission: mission.latest ? {
9149
9461
  id: mission.latest.id || null,
9150
9462
  name: mission.name,
9463
+ objective: mission.objective,
9151
9464
  state: mission.state,
9465
+ runner: mission.runner,
9466
+ model: mission.model,
9467
+ worktree_root: mission.worktree_root,
9468
+ worktree_branch: mission.worktree_branch,
9469
+ task_ref: mission.task_ref,
9470
+ current_task_id: mission.current_task_id,
9152
9471
  last_tick_at: mission.last_tick_at,
9153
9472
  last_tick_age: mission.last_tick_age,
9154
9473
  live: mission.live_mission,
9474
+ driver: mission.driver,
9155
9475
  } : null,
9156
9476
  last_activity: activity,
9157
9477
  verdict,
@@ -9431,13 +9751,1142 @@ async function memberChat(name, ...rest) {
9431
9751
  return { ok: true };
9432
9752
  }
9433
9753
 
9754
+ const IDEA_ARCHETYPE_RULES = [
9755
+ {
9756
+ slugs: ['auto-improver', 'improver'],
9757
+ pattern: /\b(repeat|recurring|broken\s+test|flaky|error|regression|pain|fix\s+bug|self[- ]improv|crash)\b/i,
9758
+ reason: 'handles recurring errors, regressions, and test fixes',
9759
+ },
9760
+ {
9761
+ slugs: ['validator', 'orb'],
9762
+ pattern: /\b(review|audit|validate|verify|check\s+diff|quality|slop|security)\b/i,
9763
+ reason: 'reviews changes and blocks weak work before merge',
9764
+ },
9765
+ {
9766
+ slugs: ['architect'],
9767
+ pattern: /\b(architect|migration|schema|api\s+design|refactor|boundary)\b/i,
9768
+ reason: 'judges architecture and multi-file design before code changes',
9769
+ },
9770
+ {
9771
+ slugs: ['navigator', 'task-planner'],
9772
+ pattern: /\b(roadmap|plan|spec|break\s+down|map|where\s+is|navigate)\b/i,
9773
+ reason: 'maps the codebase and breaks ideas into clear steps',
9774
+ },
9775
+ {
9776
+ slugs: ['customer-lead'],
9777
+ pattern: /\b(customer|churn|retention|onboarding|support|user\s+feedback|commitment)\b/i,
9778
+ reason: 'turns customer signals into clear commitments and next moves',
9779
+ },
9780
+ {
9781
+ slugs: ['researcher', 'scout'],
9782
+ pattern: /\b(research|benchmark|investigate|compare|study|survey)\b/i,
9783
+ reason: 'researches options and gathers grounded evidence',
9784
+ },
9785
+ {
9786
+ slugs: ['executor', 'builder', 'codex-executor'],
9787
+ pattern: /\b(build|implement|feature|add|wire|create|ship|code|update)\b/i,
9788
+ reason: 'builds and tests concrete product changes',
9789
+ },
9790
+ ];
9791
+
9792
+ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '') {
9793
+ const teamDir = path.join(root, 'atris', 'team');
9794
+ const members = findAllMembers(teamDir).filter(isFleetMember);
9795
+ if (!members.length) return null;
9796
+
9797
+ const cleanPreferred = String(preferredMember || '').trim().toLowerCase();
9798
+ if (cleanPreferred) {
9799
+ const match = members.find((m) => memberSlugFromRecord(m).toLowerCase() === cleanPreferred);
9800
+ if (match) {
9801
+ const slug = memberSlugFromRecord(match);
9802
+ return {
9803
+ member: slug,
9804
+ role: match.role || 'Team Member',
9805
+ reason: 'selected directly by you',
9806
+ score: 100,
9807
+ };
9808
+ }
9809
+ }
9810
+
9811
+ const text = String(ideaText || '').trim();
9812
+ const words = text
9813
+ .toLowerCase()
9814
+ .split(/[^a-z0-9]+/)
9815
+ .filter((w) => w.length >= 4);
9816
+
9817
+ let best = null;
9818
+ for (const record of members) {
9819
+ const slug = memberSlugFromRecord(record);
9820
+ let score = 0;
9821
+ let matchedReason = '';
9822
+
9823
+ for (const rule of IDEA_ARCHETYPE_RULES) {
9824
+ if (rule.slugs.includes(slug) && rule.pattern.test(text)) {
9825
+ score += 25 + (rule.slugs[0] === slug ? 5 : 0);
9826
+ matchedReason = rule.reason;
9827
+ break;
9828
+ }
9829
+ }
9830
+
9831
+ const missionFile = path.join(teamDir, slug, 'MISSION.md');
9832
+ const missionText = fs.existsSync(missionFile) ? fs.readFileSync(missionFile, 'utf8') : '';
9833
+ const corpus = `${slug} ${record.role || ''} ${record.description || ''} ${(record.skills || []).join(' ')} ${missionText}`.toLowerCase();
9834
+
9835
+ let overlapCount = 0;
9836
+ for (const word of words) {
9837
+ if (corpus.includes(word)) overlapCount += 1;
9838
+ }
9839
+ score += overlapCount * 4;
9840
+ if (Number(record.skillCount || 0) > 0) score += 2;
9841
+ if (slug === 'executor') score += 3;
9842
+ if (slug === 'auto-improver') score += 2;
9843
+
9844
+ if (!matchedReason) {
9845
+ matchedReason = record.description
9846
+ ? compactSentence(record.description, 90)
9847
+ : `fits ${record.role || slug} responsibilities`;
9848
+ }
9849
+
9850
+ const candidate = {
9851
+ member: slug,
9852
+ role: record.role || 'Team Member',
9853
+ reason: matchedReason,
9854
+ score,
9855
+ };
9856
+ if (!best || candidate.score > best.score || (candidate.score === best.score && candidate.member.localeCompare(best.member) < 0)) {
9857
+ best = candidate;
9858
+ }
9859
+ }
9860
+
9861
+ return best;
9862
+ }
9863
+
9864
+ function memberDispatch(...args) {
9865
+ if (!args.length || hasFlag(args, '--help') || hasFlag(args, '-h')) {
9866
+ console.log('Usage: atris member dispatch "<idea>" [--to <member>] [--engine <name>] [--ignite] [--dry-run] [--json]');
9867
+ console.log('Routes your idea to the best-suited team member and starts the work in an isolated checkout.');
9868
+ return;
9869
+ }
9870
+ const asJson = hasFlag(args, '--json');
9871
+ const dryRun = hasFlag(args, '--dry-run');
9872
+ const preferredMember = readFlag(args, '--to', '') || readFlag(args, '--member', '');
9873
+ const valueFlags = [...MEMBER_RUN_START_VALUE_FLAGS, '--to', '--member'];
9874
+ const boolFlags = [...MEMBER_RUN_START_BOOLEAN_FLAGS, '--dry-run'];
9875
+ const ideaText = stripKnownFlags(args, valueFlags, boolFlags).join(' ').trim();
9876
+ if (!ideaText) {
9877
+ console.error('atris member dispatch: idea text is required.');
9878
+ process.exitCode = 2;
9879
+ return;
9880
+ }
9881
+
9882
+ const picked = pickMemberForIdea(ideaText, process.cwd(), preferredMember);
9883
+ if (!picked) {
9884
+ console.error('No active team members found in atris/team/.');
9885
+ process.exitCode = 1;
9886
+ return;
9887
+ }
9888
+
9889
+ const forwardedArgs = [];
9890
+ for (let i = 0; i < args.length; i += 1) {
9891
+ const token = String(args[i] || '');
9892
+ if (token === '--dry-run') continue;
9893
+ if (token === '--to' || token === '--member') {
9894
+ i += 1;
9895
+ continue;
9896
+ }
9897
+ if (token.startsWith('--to=') || token.startsWith('--member=')) continue;
9898
+ forwardedArgs.push(args[i]);
9899
+ }
9900
+
9901
+ const commandPreview = `atris member run ${picked.member} ${JSON.stringify(ideaText)} --ignite`;
9902
+ if (dryRun) {
9903
+ const payload = {
9904
+ ok: true,
9905
+ action: 'member_dispatch',
9906
+ dry_run: true,
9907
+ idea: ideaText,
9908
+ member: picked.member,
9909
+ role: picked.role,
9910
+ reason: picked.reason,
9911
+ command: commandPreview,
9912
+ };
9913
+ printJsonOrText(
9914
+ payload,
9915
+ [
9916
+ `${picked.member} (${picked.role}) will take this because it ${picked.reason.replace(/^[A-Z]/, (c) => c.toLowerCase())}.`,
9917
+ '',
9918
+ commandPreview,
9919
+ ],
9920
+ asJson,
9921
+ );
9922
+ return payload;
9923
+ }
9924
+
9925
+ if (!asJson) {
9926
+ console.log(`${picked.member} (${picked.role}) is taking this because it ${picked.reason.replace(/^[A-Z]/, (c) => c.toLowerCase())}.`);
9927
+ }
9928
+ // --ignite means do the work now, the same as the previewed member run;
9929
+ // without it dispatch only opens the mission.
9930
+ if (hasFlag(args, '--ignite') || hasFlag(args, '--wait') || hasFlag(args, '--sync')) {
9931
+ return memberRun(picked.member, ...forwardedArgs);
9932
+ }
9933
+ return startMemberRunMission(picked.member, ideaText, forwardedArgs);
9934
+ }
9935
+
9936
+ function scrubFeedJargon(text) {
9937
+ return String(text || '')
9938
+ .replace(/\bmission-\d{4}-\d{2}-\d{2}-[a-z0-9-]+\b/gi, '')
9939
+ .replace(/\b[0-9a-f]{7,40}\b/g, '')
9940
+ .replace(/\s+/g, ' ')
9941
+ .trim();
9942
+ }
9943
+
9944
+ function plainFeedStatusLabel(mission) {
9945
+ const status = String(mission?.status || '').toLowerCase();
9946
+ if (mission?.verifier_result?.passed && (status === 'complete' || status === 'ready')) return 'verified and ready';
9947
+ if (status === 'complete' || status === 'ready') return 'ready for review';
9948
+ if (status === 'running') return 'working now';
9949
+ if (status === 'blocked' || status === 'paused') return 'needs your input';
9950
+ return 'queued';
9951
+ }
9952
+
9953
+ function buildMemberImpactChain({ summary, verified, goalWhy, nextPush, skills = [] }) {
9954
+ const action = compactSentence(scrubFeedJargon(summary || 'Shipped one bounded slice'), 100);
9955
+ const rawConsequence = verified
9956
+ ? (goalWhy ? scrubFeedJargon(goalWhy) : 'Verified check passed so this slice stays solid')
9957
+ : (goalWhy ? scrubFeedJargon(goalWhy) : 'Work is in flight toward a verified check');
9958
+ const consequence = compactSentence(rawConsequence, 100);
9959
+ const skillHint = Array.isArray(skills) && skills.length ? `Sharpens ${skills[0]} and queues the next optimization` : 'Queues the next optimization automatically';
9960
+ const compounding = compactSentence(scrubFeedJargon(nextPush || skillHint), 110);
9961
+ return {
9962
+ action,
9963
+ consequence,
9964
+ compounding,
9965
+ arrow: `${action} -> ${consequence} -> ${compounding}`,
9966
+ };
9967
+ }
9968
+
9969
+ function collectMemberWorkFeed(root = process.cwd(), options = {}) {
9970
+ const teamDir = path.join(root, 'atris', 'team');
9971
+ const members = findAllMembers(teamDir).filter(isFleetMember);
9972
+ const roleByMember = new Map(members.map((m) => [memberSlugFromRecord(m), m.role || 'Team Member']));
9973
+ const metaByMember = new Map();
9974
+ for (const m of members) {
9975
+ const slug = memberSlugFromRecord(m);
9976
+ const mDir = path.join(teamDir, slug);
9977
+ const skills = listMemberUnlockedSkills(mDir, m.skills);
9978
+ let goalWhy = null;
9979
+ let goalTitle = null;
9980
+ try {
9981
+ const gp = memberGoalPlaneStatus(slug, { memberDir: mDir, goalsFile: path.join(mDir, 'goals.json') });
9982
+ goalWhy = gp?.goal?.why || null;
9983
+ goalTitle = gp?.goal?.title || null;
9984
+ } catch {}
9985
+ metaByMember.set(slug, { skills, goalWhy, goalTitle });
9986
+ }
9987
+ const filterMember = String(options.member || '').trim().toLowerCase();
9988
+ const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0 ? Number(options.limit) : 15;
9989
+
9990
+ const items = [];
9991
+ const missionMap = memberRunMissionMap(root);
9992
+ for (const mission of missionMap.values()) {
9993
+ const owner = String(mission?.owner || '').trim();
9994
+ if (!owner) continue;
9995
+ if (filterMember && owner.toLowerCase() !== filterMember) continue;
9996
+ const atIso = mission.last_tick_at || mission.updated_at || mission.created_at || null;
9997
+ const atMs = timestampMs(atIso);
9998
+ const summaryText = scrubFeedJargon(mission.last_summary || mission.objective || '');
9999
+ if (!summaryText) continue;
10000
+ const cleanSummary = compactSentence(summaryText, 140);
10001
+ const verified = Boolean(mission?.verifier_result?.passed);
10002
+ const meta = metaByMember.get(owner) || { skills: [], goalWhy: null, goalTitle: null };
10003
+ const nextPush = scrubFeedJargon(mission.last_next_push || meta.goalTitle || '');
10004
+ const impactChain = buildMemberImpactChain({
10005
+ summary: cleanSummary,
10006
+ verified,
10007
+ goalWhy: meta.goalWhy,
10008
+ nextPush,
10009
+ skills: meta.skills,
10010
+ });
10011
+ items.push({
10012
+ member: owner,
10013
+ role: roleByMember.get(owner) || 'Team Member',
10014
+ at: atIso,
10015
+ at_ms: atMs,
10016
+ age: ageLabelFromMs(atMs),
10017
+ summary: cleanSummary,
10018
+ status: plainFeedStatusLabel(mission),
10019
+ verified,
10020
+ next_push: impactChain.compounding,
10021
+ impact_chain: impactChain,
10022
+ });
10023
+ }
10024
+
10025
+ for (const record of members) {
10026
+ const slug = memberSlugFromRecord(record);
10027
+ if (filterMember && slug.toLowerCase() !== filterMember) continue;
10028
+ const logsDir = path.join(teamDir, slug, 'logs');
10029
+ if (!fs.existsSync(logsDir)) continue;
10030
+ const logFiles = fs.readdirSync(logsDir).filter((f) => f.endsWith('.md')).sort().slice(-2);
10031
+ for (const file of logFiles) {
10032
+ const fullPath = path.join(logsDir, file);
10033
+ let raw = '';
10034
+ let mtimeMs = 0;
10035
+ try {
10036
+ raw = fs.readFileSync(fullPath, 'utf8');
10037
+ mtimeMs = fs.statSync(fullPath).mtimeMs || 0;
10038
+ } catch {
10039
+ continue;
10040
+ }
10041
+ const sections = raw.split(/^##\s+/m).slice(1);
10042
+ for (const section of sections.slice(-3)) {
10043
+ const lines = section.trim().split(/\r?\n/);
10044
+ const header = lines[0] || '';
10045
+ const summaryLine = lines.find((l) => /^-\s*(summary|mission|title):/i.test(l));
10046
+ const verifierLine = lines.find((l) => /^-\s*verifier:/i.test(l));
10047
+ const nextPushLine = lines.find((l) => /^-\s*(next_push|next push|next):/i.test(l));
10048
+ const rawSummary = summaryLine
10049
+ ? summaryLine.replace(/^-\s*(summary|mission|title):\s*/i, '')
10050
+ : header.replace(/^\d{2}:\d{2}\s*·\s*/, '');
10051
+ const cleanSummary = compactSentence(scrubFeedJargon(rawSummary), 140);
10052
+ if (!cleanSummary) continue;
10053
+ const alreadyInItems = items.some((item) => item.member === slug && item.summary === cleanSummary);
10054
+ if (alreadyInItems) continue;
10055
+ const passed = verifierLine ? /passed/i.test(verifierLine) : false;
10056
+ const meta = metaByMember.get(slug) || { skills: [], goalWhy: null, goalTitle: null };
10057
+ const rawNextPush = nextPushLine
10058
+ ? nextPushLine.replace(/^-\s*(next_push|next push|next):\s*/i, '')
10059
+ : (meta.goalTitle || '');
10060
+ const impactChain = buildMemberImpactChain({
10061
+ summary: cleanSummary,
10062
+ verified: passed,
10063
+ goalWhy: meta.goalWhy,
10064
+ nextPush: rawNextPush,
10065
+ skills: meta.skills,
10066
+ });
10067
+ items.push({
10068
+ member: slug,
10069
+ role: record.role || 'Team Member',
10070
+ at: new Date(mtimeMs).toISOString(),
10071
+ at_ms: mtimeMs,
10072
+ age: ageLabelFromMs(mtimeMs),
10073
+ summary: cleanSummary,
10074
+ status: passed ? 'verified and ready' : 'logged update',
10075
+ verified: passed,
10076
+ next_push: impactChain.compounding,
10077
+ impact_chain: impactChain,
10078
+ });
10079
+ }
10080
+ }
10081
+ }
10082
+
10083
+ return items
10084
+ .sort((a, b) => (b.at_ms || 0) - (a.at_ms || 0))
10085
+ .slice(0, limit);
10086
+ }
10087
+
10088
+ function memberFeed(...args) {
10089
+ if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
10090
+ console.log('Usage: atris member feed [--member <name>] [--limit N] [--json]');
10091
+ console.log('Shows a plain-language linear feed of real work across your team.');
10092
+ return;
10093
+ }
10094
+ const asJson = hasFlag(args, '--json');
10095
+ const memberFilter = readFlag(args, '--member', '') || readFlag(args, '--author', '');
10096
+ const limit = readNumberFlag(args, '--limit', 15);
10097
+ const feed = collectMemberWorkFeed(process.cwd(), { member: memberFilter, limit });
10098
+ const lines = !feed.length
10099
+ ? ['No team work recorded yet. Run: atris member dispatch "<idea>"']
10100
+ : [
10101
+ 'Team work feed',
10102
+ ...feed.flatMap((item) => [
10103
+ `${item.age.padEnd(4)} · ${item.member} (${item.role}): ${item.summary} [${item.status}]`,
10104
+ ` -> ${item.impact_chain.consequence} -> Next push: ${item.impact_chain.compounding}`,
10105
+ ]),
10106
+ ];
10107
+ printJsonOrText(
10108
+ {
10109
+ ok: true,
10110
+ action: 'member_feed',
10111
+ items: feed,
10112
+ },
10113
+ lines,
10114
+ asJson,
10115
+ );
10116
+ return feed;
10117
+ }
10118
+
10119
+ function extractMemberNorthStar(memberDir, fallbackDescription = '') {
10120
+ const missionPath = path.join(memberDir, 'MISSION.md');
10121
+ if (fs.existsSync(missionPath)) {
10122
+ try {
10123
+ const raw = fs.readFileSync(missionPath, 'utf8');
10124
+ const northStarMatch = raw.match(/##\s+North Star\s+([^\n#]+)/i);
10125
+ if (northStarMatch && northStarMatch[1].trim()) {
10126
+ return compactSentence(northStarMatch[1].trim(), 160);
10127
+ }
10128
+ const firstPara = raw
10129
+ .split(/\r?\n/)
10130
+ .map((l) => l.trim())
10131
+ .find((l) => l && !l.startsWith('#') && !l.startsWith('-'));
10132
+ if (firstPara) return compactSentence(firstPara, 160);
10133
+ } catch {}
10134
+ }
10135
+ return compactSentence(fallbackDescription || 'Execute bounded, verified work in this member lane.', 160);
10136
+ }
10137
+
10138
+ function listMemberUnlockedSkills(memberDir, frontmatterSkills = []) {
10139
+ const skills = new Set();
10140
+ const skillsDir = path.join(memberDir, 'skills');
10141
+ if (fs.existsSync(skillsDir)) {
10142
+ try {
10143
+ for (const entry of fs.readdirSync(skillsDir)) {
10144
+ if (fs.existsSync(path.join(skillsDir, entry, 'SKILL.md'))) skills.add(entry);
10145
+ }
10146
+ } catch {}
10147
+ }
10148
+ for (const s of Array.isArray(frontmatterSkills) ? frontmatterSkills : []) {
10149
+ if (s && typeof s === 'string') skills.add(s.trim());
10150
+ }
10151
+ return [...skills];
10152
+ }
10153
+
10154
+ function resolveMemberEngineSeat(memberSlug, root = process.cwd()) {
10155
+ const slug = String(memberSlug || '').trim().toLowerCase();
10156
+ const engineRole = slug === 'navigator'
10157
+ ? 'navigator'
10158
+ : (slug === 'validator' || slug === 'architect')
10159
+ ? 'validator'
10160
+ : 'executor';
10161
+ try {
10162
+ const { resolveEngineForRoleRanked } = require('../lib/engine-registry');
10163
+ const picked = resolveEngineForRoleRanked(engineRole, root, { taskType: engineRole });
10164
+ if (picked && picked.engine) {
10165
+ return {
10166
+ engine_id: picked.engine.id,
10167
+ tier: picked.engine.tier || 'pro',
10168
+ models: Array.isArray(picked.engine.models) ? picked.engine.models : [],
10169
+ role: engineRole,
10170
+ reason: picked.reason || `${picked.engine.id} selected for ${engineRole}.`,
10171
+ };
10172
+ }
10173
+ } catch {}
10174
+ return {
10175
+ engine_id: 'claude',
10176
+ tier: 'pro',
10177
+ models: ['claude'],
10178
+ role: engineRole,
10179
+ reason: 'Default engine seat.',
10180
+ };
10181
+ }
10182
+
10183
+ function buildMemberSubagentShell(owner, role, engineId, objective, root = process.cwd()) {
10184
+ const cleanObjective = compactSentence(objective || `Advance ${owner} roadmap`, 160);
10185
+ const verify = inferMemberRunVerifier(owner, cleanObjective, root);
10186
+ return {
10187
+ role: `${owner} (${role || 'Team Member'})`,
10188
+ member: owner,
10189
+ engine: engineId || 'claude',
10190
+ objective: cleanObjective,
10191
+ verify,
10192
+ prompt: `You are ${owner} (${role || 'Team Member'}). Read atris/team/${owner}/MEMBER.md, MISSION.md, goals.json, and skills/. Execute one bounded slice for: "${cleanObjective}". Run verify: ${verify}. Report 3 lines: Found, Fixed, Next push.`,
10193
+ shell_command: `atris member run ${owner} ${JSON.stringify(cleanObjective)} --engine ${engineId || 'claude'} --ignite`,
10194
+ };
10195
+ }
10196
+
10197
+ function buildMemberCard(name, root = process.cwd()) {
10198
+ const paths = requireMemberDir(name);
10199
+ const owner = paths.storageName || name;
10200
+ let fm = {};
10201
+ try {
10202
+ fm = parseFrontmatter(fs.readFileSync(paths.memberFile, 'utf8')) || {};
10203
+ } catch {}
10204
+ const goalPlane = memberGoalPlaneStatus(owner, paths);
10205
+ const mission = memberMissionSummary(owner, memberRunMissionMap(root), root);
10206
+ const activity = memberLastActivity(owner, paths, root);
10207
+ const verdict = memberVerdict({ mission, activity });
10208
+ const skills = listMemberUnlockedSkills(paths.memberDir, fm.skills);
10209
+ const northStar = extractMemberNorthStar(paths.memberDir, fm.description);
10210
+ const recentWork = collectMemberWorkFeed(root, { member: owner, limit: 3 });
10211
+ const engineSeat = resolveMemberEngineSeat(owner, root);
10212
+
10213
+ const doingNow = mission.latest
10214
+ ? {
10215
+ summary: compactSentence(scrubFeedJargon(mission.latest.last_summary || mission.objective || mission.name || ''), 120),
10216
+ state: plainFeedStatusLabel(mission.latest),
10217
+ engine: mission.runner || engineSeat.engine_id || null,
10218
+ model: mission.model || null,
10219
+ worktree_branch: mission.worktree_branch || null,
10220
+ last_tick_age: mission.last_tick_age,
10221
+ }
10222
+ : null;
10223
+
10224
+ const activeGoalTitle = goalPlane.goal?.title || null;
10225
+ const activeGoalWhy = goalPlane.goal?.why || null;
10226
+ const activeGoalNextPush = goalPlane.goal?.next_push || null;
10227
+ const latestItem = recentWork[0] || null;
10228
+ const isVerified = doingNow?.state === 'verified and ready' || Boolean(latestItem?.verified);
10229
+ const impactChain = buildMemberImpactChain({
10230
+ summary: doingNow?.summary || latestItem?.summary || activeGoalTitle || northStar,
10231
+ verified: isVerified,
10232
+ goalWhy: activeGoalWhy || northStar,
10233
+ nextPush: mission.latest?.last_next_push || activeGoalNextPush || activeGoalTitle,
10234
+ skills,
10235
+ });
10236
+
10237
+ const nextMove = mission.live
10238
+ ? `atris member run ${owner} --mission ${mission.live.id} --ignite`
10239
+ : activeGoalTitle
10240
+ ? `atris member run ${owner} ${JSON.stringify(activeGoalTitle)} --ignite`
10241
+ : `atris interview ${owner}`;
10242
+ const roleLabel = fm.role || 'Team Member';
10243
+ const subagentShell = buildMemberSubagentShell(
10244
+ owner,
10245
+ roleLabel,
10246
+ engineSeat?.engine_id || 'claude',
10247
+ impactChain.compounding || activeGoalTitle || northStar,
10248
+ root,
10249
+ );
10250
+
10251
+ return {
10252
+ member: owner,
10253
+ role: roleLabel,
10254
+ version: fm.version || '1.0.0',
10255
+ verdict,
10256
+ last_activity_age: activity.age,
10257
+ north_star: northStar,
10258
+ engine_seat: engineSeat,
10259
+ skills,
10260
+ impact_chain: impactChain,
10261
+ next_push: impactChain.compounding,
10262
+ subagent_shell: subagentShell,
10263
+ roadmap: {
10264
+ active_goal: activeGoalTitle,
10265
+ why: activeGoalWhy,
10266
+ acceptance: goalPlane.goal?.acceptance || [],
10267
+ doing_now: doingNow,
10268
+ },
10269
+ actions: {
10270
+ go: `atris member go ${owner}`,
10271
+ steer: `atris member steer ${owner} "<direction>"`,
10272
+ one_on_one: `atris member 1on1 ${owner}`,
10273
+ sleep: `atris member sleep ${owner}`,
10274
+ },
10275
+ recent_work: recentWork,
10276
+ next_move: nextMove,
10277
+ };
10278
+ }
10279
+
10280
+ function renderMemberCardLines(card) {
10281
+ const skillsLabel = card.skills.length ? card.skills.join(', ') : 'none yet';
10282
+ const engineLabel = card.engine_seat ? `${card.engine_seat.engine_id} (${card.engine_seat.tier})` : 'auto';
10283
+ const goalLabel = card.roadmap.active_goal || 'none set yet (run atris member 1on1 ' + card.member + ')';
10284
+ const doingLabel = card.roadmap.doing_now
10285
+ ? `${card.roadmap.doing_now.summary} [${card.roadmap.doing_now.state}]`
10286
+ : 'idle, ready for next dispatch';
10287
+ const recentLines = card.recent_work.length
10288
+ ? card.recent_work.map((item) => ` - ${item.age}: ${item.summary} [${item.status}]`)
10289
+ : [' - no verified runs logged yet'];
10290
+
10291
+ return [
10292
+ `┌─ ${card.member} · ${card.role} (v${card.version}) · ${card.verdict.toLowerCase()} · engine: ${engineLabel}`,
10293
+ `│ North Star: ${card.north_star}`,
10294
+ `│ Skills: ${skillsLabel}`,
10295
+ `│ Roadmap: ${goalLabel}`,
10296
+ `│ Doing now: ${doingLabel}`,
10297
+ `│ Loop: ${card.impact_chain.arrow}`,
10298
+ `│ Next push: ${card.next_push}`,
10299
+ `│ Recent work:`,
10300
+ ...recentLines,
10301
+ `└─ Controls: go: ${card.actions.go} | 1on1: ${card.actions.one_on_one} | sleep: ${card.actions.sleep}`,
10302
+ ];
10303
+ }
10304
+
10305
+ function memberCard(name, ...args) {
10306
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || name === '--help' || name === '-h') {
10307
+ console.log('Usage: atris member card <name|--all> [--json]');
10308
+ console.log('Shows a member\'s long-horizon roadmap, unlocked skills, current work, and recent wins.');
10309
+ return;
10310
+ }
10311
+ const asJson = hasFlag(args, '--json') || name === '--json';
10312
+ if (!name || name === '--all' || name === 'all' || name === '--json') {
10313
+ const teamDir = path.join(process.cwd(), 'atris', 'team');
10314
+ const coreOrder = ['auto-improver', 'navigator', 'executor', 'validator', 'architect', 'customer-lead', 'researcher'];
10315
+ const allSlugs = findAllMembers(teamDir)
10316
+ .filter(isFleetMember)
10317
+ .map(memberSlugFromRecord);
10318
+ const ordered = [
10319
+ ...coreOrder.filter((s) => allSlugs.includes(s)),
10320
+ ...allSlugs.filter((s) => !coreOrder.includes(s) && hasFlag(args, '--all')),
10321
+ ];
10322
+ const cards = ordered.map((slug) => buildMemberCard(slug, process.cwd()));
10323
+ printJsonOrText(
10324
+ { ok: true, action: 'member_card_all', cards },
10325
+ cards.flatMap((c) => [...renderMemberCardLines(c), '']),
10326
+ asJson,
10327
+ );
10328
+ return cards;
10329
+ }
10330
+ const card = buildMemberCard(name, process.cwd());
10331
+ printJsonOrText(
10332
+ { ok: true, action: 'member_card', card },
10333
+ renderMemberCardLines(card),
10334
+ asJson,
10335
+ );
10336
+ return card;
10337
+ }
10338
+
10339
+ function isKnownMemberSlug(token, root = process.cwd()) {
10340
+ const clean = String(token || '').trim();
10341
+ if (!clean || clean.startsWith('-')) return false;
10342
+ return fs.existsSync(path.join(root, 'atris', 'team', clean, 'MEMBER.md'))
10343
+ || fs.existsSync(path.join(root, 'atris', 'team', `${clean}.md`));
10344
+ }
10345
+
10346
+ async function memberSteerOrDispatch(...rawArgs) {
10347
+ if (hasFlag(rawArgs, '--help') || hasFlag(rawArgs, '-h')) {
10348
+ console.log('Usage: atris member steer [member] "<direction>" [--dry-run] [--json]');
10349
+ console.log(' atris member "<direction>" [--dry-run] [--json]');
10350
+ console.log('Steers a running member mid-flight or dispatches the best specialist if idle.');
10351
+ return;
10352
+ }
10353
+ const root = process.cwd();
10354
+ const asJson = hasFlag(rawArgs, '--json');
10355
+ const dryRun = hasFlag(rawArgs, '--dry-run');
10356
+ const valueFlags = new Set(['--engine', '--model', '--verify', '--referee-engine', '--minutes', '--hours', '--max-ticks', '--from']);
10357
+ const positionals = [];
10358
+ const flagArgs = [];
10359
+ for (let i = 0; i < rawArgs.length; i++) {
10360
+ const token = String(rawArgs[i] || '');
10361
+ if (!token) continue;
10362
+ if (token.startsWith('-')) {
10363
+ flagArgs.push(token);
10364
+ if (valueFlags.has(token) && i + 1 < rawArgs.length) {
10365
+ flagArgs.push(rawArgs[i + 1]);
10366
+ i++;
10367
+ }
10368
+ continue;
10369
+ }
10370
+ positionals.push(token);
10371
+ }
10372
+
10373
+ let targetMember = null;
10374
+ let prompt = '';
10375
+ let whyPicked = '';
10376
+ if (positionals.length > 0 && isKnownMemberSlug(positionals[0], root)) {
10377
+ targetMember = positionals[0];
10378
+ prompt = positionals.slice(1).join(' ').trim();
10379
+ whyPicked = `Explicitly targeted ${targetMember}.`;
10380
+ } else {
10381
+ prompt = positionals.join(' ').trim();
10382
+ const picked = pickMemberForIdea(prompt, root);
10383
+ targetMember = picked.member;
10384
+ whyPicked = picked.reason || picked.why || `fits ${picked.member} responsibilities`;
10385
+ }
10386
+
10387
+ const paths = requireMemberDir(targetMember);
10388
+ const owner = paths.storageName || targetMember;
10389
+ let fm = {};
10390
+ try {
10391
+ fm = parseFrontmatter(fs.readFileSync(paths.memberFile, 'utf8')) || {};
10392
+ } catch {}
10393
+ const goalPlane = memberGoalPlaneStatus(owner, paths);
10394
+ const effectivePrompt = prompt
10395
+ || goalPlane.goal?.next_push
10396
+ || goalPlane.goal?.title
10397
+ || extractMemberNorthStar(paths.memberDir, '');
10398
+
10399
+ const missionMap = memberRunMissionMap(root);
10400
+ const missionSummary = memberMissionSummary(owner, missionMap, root);
10401
+ const engineSeat = resolveMemberEngineSeat(owner, root);
10402
+ const { autoQueueMemberNextPush, closeMemberHandoffsForReceiver } = require('./mission');
10403
+ const subagentShell = buildMemberSubagentShell(owner, fm.role || 'Team Member', engineSeat.engine_id, effectivePrompt, root);
10404
+
10405
+ if (effectivePrompt) {
10406
+ try {
10407
+ autoQueueMemberNextPush(owner, effectivePrompt, missionSummary.live?.objective || '', root);
10408
+ } catch {}
10409
+ }
10410
+ if (!dryRun) {
10411
+ try {
10412
+ closeMemberHandoffsForReceiver(owner, root);
10413
+ } catch {}
10414
+ }
10415
+
10416
+ if (missionSummary.live) {
10417
+ if (!dryRun) {
10418
+ try {
10419
+ const liveM = missionSummary.live;
10420
+ const targetRoot = liveM.worktree_root || root;
10421
+ const missionsFile = path.join(targetRoot, '.atris', 'state', 'missions.jsonl');
10422
+ const nextPings = [
10423
+ ...(Array.isArray(liveM.pings) ? liveM.pings : []),
10424
+ { at: new Date().toISOString(), from: 'operator', text: effectivePrompt },
10425
+ ];
10426
+ const updatedMission = {
10427
+ ...liveM,
10428
+ pings: nextPings,
10429
+ last_next_push: effectivePrompt,
10430
+ updated_at: new Date().toISOString(),
10431
+ };
10432
+ fs.mkdirSync(path.dirname(missionsFile), { recursive: true });
10433
+ fs.appendFileSync(missionsFile, `${JSON.stringify(updatedMission)}\n`, 'utf8');
10434
+ } catch {}
10435
+ }
10436
+ const payload = {
10437
+ ok: true,
10438
+ action: 'member_steered',
10439
+ mode: 'steered_live',
10440
+ dry_run: dryRun,
10441
+ member: owner,
10442
+ mission_id: missionSummary.live.id,
10443
+ engine: missionSummary.runner || engineSeat.engine_id,
10444
+ prompt: effectivePrompt,
10445
+ why: whyPicked,
10446
+ subagent_shell: subagentShell,
10447
+ };
10448
+ const lines = [
10449
+ `Steered ${owner} mid-run (${missionSummary.live.id}).`,
10450
+ `Direction: ${effectivePrompt}`,
10451
+ `Consequence: ${owner} will read this steer on the next loop and adjust course.`,
10452
+ ];
10453
+ printJsonOrText(payload, lines, asJson);
10454
+ return payload;
10455
+ }
10456
+
10457
+ const command = `atris member run ${owner} ${JSON.stringify(effectivePrompt)} --engine ${engineSeat.engine_id} --ignite`;
10458
+ if (dryRun) {
10459
+ const payload = {
10460
+ ok: true,
10461
+ action: 'member_go',
10462
+ mode: 'dispatched_idle',
10463
+ dry_run: true,
10464
+ member: owner,
10465
+ engine: engineSeat.engine_id,
10466
+ engine_reason: engineSeat.reason,
10467
+ prompt: effectivePrompt,
10468
+ why: whyPicked,
10469
+ command,
10470
+ subagent_shell: subagentShell,
10471
+ };
10472
+ const lines = [
10473
+ `Routed to ${owner} on engine ${engineSeat.engine_id}: ${whyPicked}`,
10474
+ `Objective: ${effectivePrompt}`,
10475
+ `Next: ${command}`,
10476
+ ];
10477
+ printJsonOrText(payload, lines, asJson);
10478
+ return payload;
10479
+ }
10480
+
10481
+ if (!asJson) {
10482
+ console.log(`Routed to ${owner} on engine ${engineSeat.engine_id}: ${whyPicked}`);
10483
+ }
10484
+ const hasExplicitEngine = flagArgs.includes('--engine');
10485
+ const nextFlags = hasExplicitEngine ? flagArgs : ['--engine', engineSeat.engine_id, ...flagArgs];
10486
+ return memberRun(owner, effectivePrompt, '--ignite', ...nextFlags);
10487
+ }
10488
+
10489
+ async function memberGo(...rawArgs) {
10490
+ if (hasFlag(rawArgs, '--help') || hasFlag(rawArgs, '-h')) {
10491
+ console.log('Usage: atris member go [member|"<direction>"] [--dry-run] [--json]');
10492
+ console.log('Wakes the highest-leverage member on their next_push, or routes a prompt to the right specialist.');
10493
+ return;
10494
+ }
10495
+ const nonFlags = rawArgs.filter((a) => a && !String(a).startsWith('-'));
10496
+ if (nonFlags.length > 0) {
10497
+ return memberSteerOrDispatch(...rawArgs);
10498
+ }
10499
+ const root = process.cwd();
10500
+ const teamDir = path.join(root, 'atris', 'team');
10501
+ const coreOrder = ['auto-improver', 'executor', 'navigator', 'validator', 'architect', 'customer-lead', 'researcher'];
10502
+ let chosen = 'auto-improver';
10503
+ for (const slug of coreOrder) {
10504
+ const mDir = path.join(teamDir, slug);
10505
+ if (!fs.existsSync(path.join(mDir, 'MEMBER.md'))) continue;
10506
+ const gp = memberGoalPlaneStatus(slug, { memberDir: mDir, goalsFile: path.join(mDir, 'goals.json') });
10507
+ if (gp?.goal?.next_push || gp?.goal?.title) {
10508
+ chosen = slug;
10509
+ break;
10510
+ }
10511
+ }
10512
+ return memberSteerOrDispatch(chosen, ...rawArgs);
10513
+ }
10514
+
10515
+ function memberSleep(target, ...args) {
10516
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || target === '--help' || target === '-h') {
10517
+ console.log('Usage: atris member sleep [member|--all] [--json]');
10518
+ console.log('Cleanly parks active member loops so your team rests until you wake them.');
10519
+ return;
10520
+ }
10521
+ const root = process.cwd();
10522
+ const asJson = hasFlag(args, '--json') || target === '--json';
10523
+ const filterMember = target && !target.startsWith('-') && target !== 'all' ? target.trim().toLowerCase() : '';
10524
+ const { loadMissionMap } = require('./mission');
10525
+ const missions = loadMissionMap(root);
10526
+ const activeStatuses = new Set(['running', 'planning', 'ready']);
10527
+ const slept = [];
10528
+
10529
+ for (const m of missions.values()) {
10530
+ if (!m || !activeStatuses.has(String(m.status || '').toLowerCase())) continue;
10531
+ const owner = String(m.owner || '').trim();
10532
+ if (filterMember && owner.toLowerCase() !== filterMember) continue;
10533
+ const updated = {
10534
+ ...m,
10535
+ status: 'paused',
10536
+ paused_at: new Date().toISOString(),
10537
+ stop_reason: 'member-sleep',
10538
+ next_action: `sleeping; wake with: atris member go ${owner || ''}`.trim(),
10539
+ updated_at: new Date().toISOString(),
10540
+ };
10541
+ const missionsFile = path.join(root, '.atris', 'state', 'missions.jsonl');
10542
+ fs.mkdirSync(path.dirname(missionsFile), { recursive: true });
10543
+ fs.appendFileSync(missionsFile, `${JSON.stringify(updated)}\n`, 'utf8');
10544
+ slept.push({ mission_id: m.id, member: owner || 'mission-lead', objective: m.objective });
10545
+ }
10546
+
10547
+ const payload = {
10548
+ ok: true,
10549
+ action: 'member_sleep',
10550
+ target: filterMember || 'all',
10551
+ slept_count: slept.length,
10552
+ slept,
10553
+ wake_command: filterMember ? `atris member go ${filterMember}` : 'atris member go',
10554
+ };
10555
+ const lines = slept.length
10556
+ ? [
10557
+ `Put ${slept.length} active member loop${slept.length === 1 ? '' : 's'} to sleep.`,
10558
+ ...slept.map((s) => ` - ${s.member}: ${s.objective}`),
10559
+ `Wake anytime: ${payload.wake_command}`,
10560
+ ]
10561
+ : [
10562
+ `All members are already resting.`,
10563
+ `Wake anytime: ${payload.wake_command}`,
10564
+ ];
10565
+ printJsonOrText(payload, lines, asJson);
10566
+ return payload;
10567
+ }
10568
+
10569
+ async function memberOneOnOne(name, ...args) {
10570
+ if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
10571
+ console.log('Usage: atris member 1on1 <name> [--note "..."]');
10572
+ console.log('Sit down for a 1-on-1 with a team member to review recent work and set their next roadmap goal.');
10573
+ return;
10574
+ }
10575
+ const { interviewCommand } = require('./interview');
10576
+ const code = await interviewCommand([name, ...args]);
10577
+ if (code) process.exitCode = code;
10578
+ return code;
10579
+ }
10580
+
10581
+ function collectMemberHandoffs(root = process.cwd(), cards = []) {
10582
+ const handoffsFile = path.join(root, '.atris', 'state', 'member-handoffs.jsonl');
10583
+ const recorded = readJsonlRowsIfExists(handoffsFile, 20)
10584
+ .filter((h) => h && h.from && h.to && (h.status || 'open') === 'open')
10585
+ .reverse();
10586
+ if (recorded.length > 0) return recorded.slice(0, 6);
10587
+ const relay = {
10588
+ researcher: 'navigator',
10589
+ 'customer-lead': 'navigator',
10590
+ navigator: 'executor',
10591
+ architect: 'executor',
10592
+ executor: 'validator',
10593
+ 'auto-improver': 'validator',
10594
+ };
10595
+ const inferred = [];
10596
+ for (const c of cards) {
10597
+ const to = relay[c.member];
10598
+ if (!to) continue;
10599
+ const state = c.roadmap?.doing_now?.state || '';
10600
+ if (state === 'verified and ready' || c.verdict === 'ACTIVE') {
10601
+ inferred.push({
10602
+ id: `relay_${c.member}_${to}`,
10603
+ from: c.member,
10604
+ to,
10605
+ summary: compactSentence(c.roadmap?.doing_now?.summary || c.roadmap?.active_goal || c.next_push, 90),
10606
+ status: 'ready',
10607
+ next_command: `atris member go ${to}`,
10608
+ });
10609
+ }
10610
+ }
10611
+ return inferred.slice(0, 4);
10612
+ }
10613
+
10614
+ function collectMemberOneOnOnes(root = process.cwd(), slugs = []) {
10615
+ const queue = [];
10616
+ for (const slug of slugs) {
10617
+ try {
10618
+ const paths = requireMemberDir(slug);
10619
+ const gp = memberGoalPlaneStatus(slug, paths);
10620
+ if (!gp.goal) {
10621
+ queue.push({
10622
+ member: slug,
10623
+ reason: 'No active roadmap goal set yet',
10624
+ command: `atris member 1on1 ${slug}`,
10625
+ });
10626
+ } else if (gp.needsUser) {
10627
+ queue.push({
10628
+ member: slug,
10629
+ reason: gp.ask || 'Blocked and waiting on your answer',
10630
+ command: `atris member 1on1 ${slug}`,
10631
+ });
10632
+ }
10633
+ } catch {}
10634
+ }
10635
+ return queue;
10636
+ }
10637
+
10638
+ function memberHandoff(from, to, ...args) {
10639
+ if (from === '--help' || from === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
10640
+ console.log('Usage: atris member handoff [<from> <to> "<slice>"] [--json]');
10641
+ console.log('Passes a completed slice from one member to the next specialist and queues their next_push.');
10642
+ return;
10643
+ }
10644
+ const root = process.cwd();
10645
+ const asJson = from === '--json' || to === '--json' || hasFlag(args, '--json');
10646
+ const sliceWords = [to, ...args].filter((a) => a && !String(a).startsWith('-'));
10647
+ if (!from || from.startsWith('-') || sliceWords.length < 2) {
10648
+ const room = buildMemberRoom(root);
10649
+ const lines = room.handoffs.length
10650
+ ? [
10651
+ 'Active Member Handoffs:',
10652
+ ...room.handoffs.map((h) => ` - ${h.from} -> ${h.to}: ${h.summary} (run: ${h.next_command})`),
10653
+ ]
10654
+ : ['No open handoffs right now.'];
10655
+ printJsonOrText({ ok: true, action: 'member_handoffs', handoffs: room.handoffs }, lines, asJson);
10656
+ return room.handoffs;
10657
+ }
10658
+ const targetTo = sliceWords[0];
10659
+ const summary = sliceWords.slice(1).join(' ').trim();
10660
+ const { recordMemberHandoff } = require('./mission');
10661
+ const handoff = recordMemberHandoff(from, targetTo, summary, root);
10662
+ if (!handoff) {
10663
+ console.error(`Could not record handoff from ${from} to ${targetTo}. Check member names.`);
10664
+ process.exitCode = 1;
10665
+ return null;
10666
+ }
10667
+ const lines = [
10668
+ `Handoff recorded: ${handoff.from} -> ${handoff.to}`,
10669
+ `Slice: ${handoff.summary}`,
10670
+ `Next: ${handoff.next_command}`,
10671
+ ];
10672
+ printJsonOrText({ ok: true, action: 'member_handoff', handoff }, lines, asJson);
10673
+ return handoff;
10674
+ }
10675
+
10676
+ function buildMemberRoom(root = process.cwd()) {
10677
+ const teamDir = path.join(root, 'atris', 'team');
10678
+ const coreOrder = ['auto-improver', 'navigator', 'executor', 'validator', 'architect', 'customer-lead', 'researcher'];
10679
+ const allSlugs = findAllMembers(teamDir)
10680
+ .filter(isFleetMember)
10681
+ .map(memberSlugFromRecord);
10682
+ const ordered = coreOrder.filter((s) => allSlugs.includes(s));
10683
+ const cards = ordered.map((slug) => buildMemberCard(slug, root));
10684
+ const feed = collectMemberWorkFeed(root, { limit: 6 });
10685
+
10686
+ const standup = cards.map((c) => {
10687
+ const status = c.roadmap.doing_now ? c.roadmap.doing_now.state : c.verdict.toLowerCase();
10688
+ const latestUpdate = c.roadmap.doing_now?.summary
10689
+ || c.recent_work[0]?.summary
10690
+ || c.roadmap.active_goal
10691
+ || c.north_star;
10692
+ return {
10693
+ member: c.member,
10694
+ role: c.role,
10695
+ status,
10696
+ engine: c.engine_seat?.engine_id || 'auto',
10697
+ skills_count: c.skills.length,
10698
+ latest_update: compactSentence(latestUpdate, 92),
10699
+ consequence: c.impact_chain.consequence,
10700
+ next_push: compactSentence(c.next_push, 84),
10701
+ subagent_shell: c.subagent_shell,
10702
+ one_on_one: `atris member 1on1 ${c.member}`,
10703
+ };
10704
+ });
10705
+
10706
+ const handoffs = collectMemberHandoffs(root, cards);
10707
+ const oneOnOnes = collectMemberOneOnOnes(root, ordered);
10708
+
10709
+ let readyEngines = [];
10710
+ try {
10711
+ const { engineRegistryView } = require('../lib/engine-registry');
10712
+ readyEngines = engineRegistryView(root)
10713
+ .filter((e) => e.health && e.health.status === 'ready')
10714
+ .map((e) => ({
10715
+ id: e.id,
10716
+ tier: e.tier,
10717
+ roles: e.roles,
10718
+ models: e.models,
10719
+ }));
10720
+ } catch {}
10721
+
10722
+ return {
10723
+ schema: 'atris.member.room.v1',
10724
+ utopia: {
10725
+ percent: 100,
10726
+ loop: 'ACTION: Plain prompt or click -> CONSEQUENCE: Verified two-model execution + SKILL.md lesson -> COMPOUNDING: Self-queued next_push + Moneyball engine routing -> UTOPIA: Autonomous team that improves forever',
10727
+ },
10728
+ engines: readyEngines,
10729
+ members: cards,
10730
+ standup,
10731
+ handoffs,
10732
+ one_on_ones: oneOnOnes,
10733
+ feed,
10734
+ controls: {
10735
+ go: 'atris member go',
10736
+ day: 'atris member day',
10737
+ handoff: 'atris member handoff <from> <to> "<slice>"',
10738
+ steer: 'atris member "<prompt>"',
10739
+ one_on_one: 'atris member 1on1 <member>',
10740
+ sleep: 'atris member sleep',
10741
+ feed: 'atris member feed',
10742
+ },
10743
+ };
10744
+ }
10745
+
10746
+ async function memberStandupWalk(room, args = []) {
10747
+ const asJson = hasFlag(args, '--json');
10748
+ const limit = Number(readNumberFlag(args, '--limit', 0)) || room.standup.length;
10749
+ const rawDecisions = readFlag(args, '--decisions', '');
10750
+ const scripted = rawDecisions
10751
+ ? rawDecisions.split('|').map((s) => s.trim())
10752
+ : [];
10753
+ const slice = room.standup.slice(0, Math.max(1, limit));
10754
+ const { autoQueueMemberNextPush } = require('./mission');
10755
+ const results = [];
10756
+
10757
+ let rl = null;
10758
+ if (!scripted.length && process.stdin.isTTY && !asJson) {
10759
+ const readline = require('readline');
10760
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10761
+ }
10762
+ const askLine = (promptText) => new Promise((resolve) => {
10763
+ if (!rl) return resolve('y');
10764
+ rl.question(promptText, (ans) => resolve(String(ans || '').trim()));
10765
+ });
10766
+
10767
+ try {
10768
+ for (let i = 0; i < slice.length; i++) {
10769
+ const s = slice[i];
10770
+ if (!asJson) {
10771
+ console.log(`\n+-----------------------------------------------------------------------+`);
10772
+ console.log(`| ${i + 1}. ${s.member.toUpperCase()} (${s.role})`);
10773
+ console.log(`| Found: ${s.latest_update}`);
10774
+ console.log(`| Push: ${s.next_push}`);
10775
+ console.log(`+-----------------------------------------------------------------------+`);
10776
+ }
10777
+ const rawAns = scripted[i] !== undefined
10778
+ ? scripted[i]
10779
+ : await askLine(' Decision [y = approve / n = skip / or type new idea]: ');
10780
+ const cleanAns = String(rawAns || 'y').trim();
10781
+ const lower = cleanAns.toLowerCase();
10782
+
10783
+ if (lower === 'n' || lower === 'no' || lower === 'skip' || lower === 'sleep') {
10784
+ results.push({ member: s.member, decision: 'skip', engine: s.engine, next_push: s.next_push });
10785
+ if (!asJson) console.log(` -> NO: skipped ${s.member} for this cycle.`);
10786
+ } else if (lower === 'y' || lower === 'yes' || lower === '') {
10787
+ autoQueueMemberNextPush(s.member, s.next_push, s.latest_update, process.cwd(), { promote: true });
10788
+ results.push({ member: s.member, decision: 'approved', engine: s.engine, next_push: s.next_push, command: `atris member go ${s.member}`, subagent_shell: s.subagent_shell });
10789
+ if (!asJson) console.log(` -> YES: approved "${s.next_push}" on ${s.engine} (run: atris member go ${s.member})`);
10790
+ } else {
10791
+ autoQueueMemberNextPush(s.member, cleanAns, 'Operator standup steer', process.cwd(), { promote: true });
10792
+ const steeredShell = buildMemberSubagentShell(s.member, s.role, s.engine, cleanAns, process.cwd());
10793
+ results.push({ member: s.member, decision: 'steered', engine: s.engine, next_push: cleanAns, command: `atris member go ${s.member}`, subagent_shell: steeredShell });
10794
+ if (!asJson) console.log(` -> IDEA: steered ${s.member} to "${cleanAns}" on ${s.engine} (run: atris member go ${s.member})`);
10795
+ }
10796
+ }
10797
+ } finally {
10798
+ if (rl) rl.close();
10799
+ }
10800
+
10801
+ const payload = {
10802
+ ok: true,
10803
+ action: 'member_standup_walk',
10804
+ reviewed: results.length,
10805
+ results,
10806
+ };
10807
+ if (asJson) {
10808
+ console.log(JSON.stringify(payload, null, 2));
10809
+ } else {
10810
+ console.log(`\nStandup complete (${results.length} members reviewed). Launch any approved member with: atris member go <name>`);
10811
+ }
10812
+ return payload;
10813
+ }
10814
+
10815
+ async function memberRoom(...args) {
10816
+ if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
10817
+ console.log('Usage: atris member [day|standup|room] [--walk] [--limit N] [--decisions "y|idea|n"] [--json]');
10818
+ console.log('Shows your Workday Briefing & Team Room: member-by-member standup, handoffs, 1-on-1 queue, and controls.');
10819
+ return;
10820
+ }
10821
+ const asJson = hasFlag(args, '--json');
10822
+ const room = buildMemberRoom(process.cwd());
10823
+ if (hasFlag(args, '--walk') || Boolean(readFlag(args, '--decisions', ''))) {
10824
+ return memberStandupWalk(room, args);
10825
+ }
10826
+ const engineSummary = room.engines.length
10827
+ ? room.engines.map((e) => `${e.id}(${e.tier})`).join(', ')
10828
+ : 'claude, codex';
10829
+
10830
+ const standupRows = room.standup.flatMap((s, idx) => [
10831
+ `+-----------------------------------------------------------------------+`,
10832
+ `| ${idx + 1}. ${s.member.toUpperCase()} (${s.role}) · ${s.status} · engine:${s.engine}`,
10833
+ `| Found: ${s.latest_update}`,
10834
+ `| Push: ${s.next_push}`,
10835
+ `+-----------------------------------------------------------------------+`,
10836
+ ]);
10837
+
10838
+ const handoffRows = room.handoffs.length
10839
+ ? room.handoffs.map((h) => ` - ${h.from} -> ${h.to}: ${h.summary} (run: ${h.next_command})`)
10840
+ : [' - No pending baton passes right now.'];
10841
+
10842
+ const oneOnOneRows = room.one_on_ones.length
10843
+ ? room.one_on_ones.map((o) => ` - ${o.member}: ${o.reason} -> ${o.command}`)
10844
+ : [' - Zero blocking 1-on-1s today. Every member has an active roadmap and engine seat.'];
10845
+
10846
+ const feedRows = room.feed.length
10847
+ ? room.feed.slice(0, 3).flatMap((item) => [
10848
+ ` ${item.age.padEnd(4)} · ${item.member}: ${item.summary} [${item.status}]`,
10849
+ ` -> ${item.impact_chain.consequence} -> Next push: ${item.impact_chain.compounding}`,
10850
+ ])
10851
+ : [' No work recorded yet. Run: atris member go'];
10852
+
10853
+ const lines = [
10854
+ 'Your team · workday briefing',
10855
+ `Engines ready: ${engineSummary}`,
10856
+ `Loop: ${room.utopia.loop}`,
10857
+ '',
10858
+ '1. Member-by-Member Standup (detail: `atris member card <name>` | rapid walk: `atris member standup --walk`):',
10859
+ ...standupRows,
10860
+ '',
10861
+ '2. Handoffs & Baton Passes (`atris member handoff <from> <to> "<slice>"`):',
10862
+ ...handoffRows,
10863
+ '',
10864
+ '3. Your 1-on-1s Today (`atris member 1on1 <name>`):',
10865
+ ...oneOnOneRows,
10866
+ '',
10867
+ 'Live Consequence Feed:',
10868
+ ...feedRows,
10869
+ '',
10870
+ 'Controls: atris member standup --walk | atris member go | atris member "<prompt>" | atris member 1on1 <name> | atris member sleep',
10871
+ ];
10872
+
10873
+ printJsonOrText({ ok: true, action: 'member_room', ...room }, lines, asJson);
10874
+ return room;
10875
+ }
10876
+
9434
10877
  async function memberCommand(subcommand, ...args) {
9435
10878
  // Subcommands that take a member name as args[0] otherwise treat `--help` as
9436
10879
  // a name and error with "Member '--help' not found". `create`/`new` handle
9437
10880
  // help themselves (with subcommand-specific usage), leave those alone.
9438
- const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run', 'install', 'result']);
9439
- if (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help')) {
9440
- subcommand = undefined;
10881
+ const HELP_AWARE_SUBCOMMANDS = new Set([
10882
+ 'create', 'new', 'run', 'install', 'result', 'dispatch', 'route', 'feed', 'stream',
10883
+ 'card', 'cards', 'go', 'work', 'steer', 'sleep', 'rest', '1on1', 'interview',
10884
+ 'room', 'ui', 'day', 'standup', 'handoff', 'handoffs',
10885
+ ]);
10886
+ const explicitHelp = subcommand === 'help' || subcommand === '--help' || subcommand === '-h'
10887
+ || (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help'));
10888
+ if (explicitHelp) {
10889
+ subcommand = 'help';
9441
10890
  }
9442
10891
  switch (subcommand) {
9443
10892
  case 'list':
@@ -9477,6 +10926,34 @@ async function memberCommand(subcommand, ...args) {
9477
10926
  if (code) process.exitCode = code;
9478
10927
  return undefined;
9479
10928
  }
10929
+ case 'go':
10930
+ case 'work':
10931
+ return memberGo(...args);
10932
+ case 'steer':
10933
+ return memberSteerOrDispatch(...args);
10934
+ case 'sleep':
10935
+ case 'rest':
10936
+ return memberSleep(args[0], ...args.slice(1));
10937
+ case '1on1':
10938
+ case 'interview':
10939
+ return memberOneOnOne(args[0], ...args.slice(1));
10940
+ case 'room':
10941
+ case 'ui':
10942
+ case 'day':
10943
+ case 'standup':
10944
+ return memberRoom(...args);
10945
+ case 'handoff':
10946
+ case 'handoffs':
10947
+ return memberHandoff(args[0], args[1], ...args.slice(2));
10948
+ case 'dispatch':
10949
+ case 'route':
10950
+ return memberDispatch(...args);
10951
+ case 'feed':
10952
+ case 'stream':
10953
+ return memberFeed(...args);
10954
+ case 'card':
10955
+ case 'cards':
10956
+ return memberCard(args[0], ...args.slice(1));
9480
10957
  case 'ping':
9481
10958
  return memberPing(args[0], ...args.slice(1));
9482
10959
  case 'loop':
@@ -9508,11 +10985,31 @@ async function memberCommand(subcommand, ...args) {
9508
10985
  return memberArchive(args[0]);
9509
10986
  case 'purge-archived':
9510
10987
  return memberPurgeArchived(...args);
10988
+ case undefined:
10989
+ case '--json':
10990
+ if (fs.existsSync(path.join(process.cwd(), 'atris', 'team'))) {
10991
+ return memberRoom(...(subcommand === '--json' ? ['--json', ...args] : args));
10992
+ }
10993
+ // fall through to help if not in a workspace with atris/team
9511
10994
  default:
10995
+ if (subcommand && subcommand !== 'help' && !subcommand.startsWith('-')) {
10996
+ // Natural prompt or member name: `atris member "make checkout faster"` or `atris member navigator`
10997
+ if (isKnownMemberSlug(subcommand, process.cwd()) && args.filter((a) => !String(a).startsWith('-')).length === 0) {
10998
+ return memberCard(subcommand, ...args);
10999
+ }
11000
+ return memberSteerOrDispatch(subcommand, ...args);
11001
+ }
9512
11002
  console.log('');
9513
- console.log('Usage: atris member <subcommand> [name]');
11003
+ console.log('Usage: atris member <subcommand|"<prompt>"> [name]');
9514
11004
  console.log('');
9515
11005
  console.log('Subcommands:');
11006
+ console.log(' (bare) Open the Team Room (members, engines, live consequence feed)');
11007
+ console.log(' "<prompt>" Steer a running member mid-flight or start the best specialist');
11008
+ console.log(' go [name|"..."] Wake the highest-leverage member on its next_push or run a prompt');
11009
+ console.log(' steer [name] "..." Steer a member mid-run or update its immediate direction');
11010
+ console.log(' 1on1 <name> Sit down for a 1-on-1 to review recent wins and set the roadmap');
11011
+ console.log(' sleep [name|--all] Park active member loops so your team rests until woken');
11012
+ console.log(' room [--json] Render the visual Team Room and desktop UI payload');
9516
11013
  console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
9517
11014
  console.log(' install <slug> [--update] Install missing files, or refresh template skills');
9518
11015
  console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
@@ -9524,9 +11021,12 @@ async function memberCommand(subcommand, ...args) {
9524
11021
  console.log(' goal <name> "..." Create/update a member long-term goal');
9525
11022
  console.log(' goal-from-mission <name> Create/reuse a goal from MISSION.md and now.md');
9526
11023
  console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
9527
- console.log(' wake <name> Read Mission state and decide tick/wait/ask/stop');
11024
+ console.log(' wake <name> Decide tick/wait/ask/stop, then run one bounded work step on the roster engine (--plan to only decide)');
9528
11025
  console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
9529
11026
  console.log(' result <name> Newest run result: run record, receipt, artifacts, number [--since <iso>] [--json]');
11027
+ console.log(' dispatch "<idea>" Route an idea to the best specialist member and start work');
11028
+ console.log(' feed [--member <n>] Show a plain-language linear feed of real work across the team');
11029
+ console.log(' card [name|--all] Show a member\'s roadmap, unlocked skills, live work, and recent wins');
9530
11030
  console.log(' loop <name|--all> Repeat wake on a bounded cadence with a no-overlap lease');
9531
11031
  console.log(' tick <name> Propose the next bounded experiment');
9532
11032
  console.log(' review <name> <id> Accept/discard an experiment with proof');
@@ -9586,10 +11086,16 @@ async function memberCommand(subcommand, ...args) {
9586
11086
 
9587
11087
  module.exports = {
9588
11088
  proposalPromptForGoal,
11089
+ appendMemberGoalLog,
11090
+ appendProjectLog,
9589
11091
  memberCommand,
9590
11092
  findAllMembers,
9591
11093
  findWorkspaceBusinessId,
9592
11094
  parseFrontmatter,
9593
11095
  wakeBootLines,
9594
11096
  buildMemberRunStartArgs,
11097
+ inferMemberRunVerifier,
11098
+ pickMemberForIdea,
11099
+ appendMemberGoalLog,
11100
+ appendProjectLog,
9595
11101
  };