atris 3.34.0 → 3.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
@@ -6,6 +6,7 @@ const { execFileSync, spawnSync } = require('child_process');
6
6
  const { loadCredentials } = require('../utils/auth');
7
7
  const { apiRequestJson } = require('../utils/api');
8
8
  const { runAliveTick } = require('../lib/member-alive');
9
+ const { defaultObjectiveRunner } = require('../lib/default-runner');
9
10
 
10
11
  function todayLogName() {
11
12
  const now = new Date();
@@ -211,6 +212,12 @@ function resolveMemberRuntime(name) {
211
212
  function memberPaths(name) {
212
213
  const teamDir = path.join(process.cwd(), 'atris', 'team');
213
214
  const memberDir = path.join(teamDir, name || '');
215
+ // Steering signals live in the workspace's single .atris/state store. Resolve
216
+ // the shared workspace root (spine -> git -> cwd) so a member run from a
217
+ // subdirectory reads steering from the real store instead of a nonexistent
218
+ // nested .atris — the same resolver the mission/task/usage/autopilot state
219
+ // uses. (teamDir markdown discovery is a separate cwd concern, filed.)
220
+ const workspaceRoot = require('../lib/mission-root').resolveWorkspaceRoot(process.cwd());
214
221
  return {
215
222
  teamDir,
216
223
  memberDir,
@@ -218,7 +225,7 @@ function memberPaths(name) {
218
225
  missionFile: path.join(memberDir, 'MISSION.md'),
219
226
  goalsJson: path.join(memberDir, 'goals.json'),
220
227
  goalsMd: path.join(memberDir, 'goals.md'),
221
- steeringJsonl: path.join(process.cwd(), '.atris', 'state', 'steering.jsonl'),
228
+ steeringJsonl: path.join(workspaceRoot, '.atris', 'state', 'steering.jsonl'),
222
229
  };
223
230
  }
224
231
 
@@ -252,6 +259,50 @@ function ensureMissionFile(memberDir, { name, role, description } = {}) {
252
259
  return missionPath;
253
260
  }
254
261
 
262
+ function listExistingMemberSlugs() {
263
+ const teamDir = path.join(process.cwd(), 'atris', 'team');
264
+ try {
265
+ return fs.readdirSync(teamDir, { withFileTypes: true })
266
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith('_'))
267
+ .map(entry => entry.name)
268
+ .filter(slug => fs.existsSync(path.join(teamDir, slug, 'MEMBER.md')))
269
+ .sort();
270
+ } catch {
271
+ return [];
272
+ }
273
+ }
274
+
275
+ function editDistance(a, b) {
276
+ const rows = a.length + 1;
277
+ const cols = b.length + 1;
278
+ const dist = Array.from({ length: rows }, (_, i) => [i, ...Array(cols - 1).fill(0)]);
279
+ for (let j = 0; j < cols; j++) dist[0][j] = j;
280
+ for (let i = 1; i < rows; i++) {
281
+ for (let j = 1; j < cols; j++) {
282
+ dist[i][j] = Math.min(
283
+ dist[i - 1][j] + 1,
284
+ dist[i][j - 1] + 1,
285
+ dist[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
286
+ );
287
+ }
288
+ }
289
+ return dist[rows - 1][cols - 1];
290
+ }
291
+
292
+ function nearestMemberSlug(name, slugs) {
293
+ const needle = String(name || '').toLowerCase();
294
+ let best = null;
295
+ let bestScore = Infinity;
296
+ for (const slug of slugs) {
297
+ const score = editDistance(needle, slug.toLowerCase());
298
+ if (score < bestScore) {
299
+ best = slug;
300
+ bestScore = score;
301
+ }
302
+ }
303
+ return bestScore <= Math.max(2, Math.floor(needle.length / 3)) ? best : null;
304
+ }
305
+
255
306
  function requireMemberDir(name) {
256
307
  if (!name) {
257
308
  console.error('Usage: atris member <goal|tick|review> <name> ...');
@@ -266,6 +317,15 @@ function requireMemberDir(name) {
266
317
  if (!fs.existsSync(paths.memberFile)) {
267
318
  const aliasHint = resolved.aliasOf ? ` or atris/team/${resolved.aliasOf}/MEMBER.md` : '';
268
319
  console.error(`Member "${name}" not found at atris/team/${name}/MEMBER.md${aliasHint}`);
320
+ const team = listExistingMemberSlugs();
321
+ const nearest = nearestMemberSlug(name, team);
322
+ if (nearest) {
323
+ console.error(`Did you mean "${nearest}"?`);
324
+ } else if (team.length) {
325
+ console.error(`Team here: ${team.join(', ')}`);
326
+ } else {
327
+ console.error(`No team members exist here yet. Create one: atris member create ${name} --role="..."`);
328
+ }
269
329
  process.exit(1);
270
330
  }
271
331
  return {
@@ -440,6 +500,170 @@ function memberRunOwnedActiveMissionId(name, missionMap) {
440
500
  return candidates[0]?.id || '';
441
501
  }
442
502
 
503
+ const MEMBER_LIVE_MISSION_STATUSES = new Set(['running', 'planning']);
504
+ const MEMBER_ACTIVE_ACTIVITY_MS = 24 * 60 * 60 * 1000;
505
+ const MEMBER_RECENT_ACTIVITY_MS = 72 * 60 * 60 * 1000;
506
+
507
+ function memberSlugFromRecord(member) {
508
+ if (member?.format === 'directory' && member.dir) return path.basename(member.dir);
509
+ if (member?.format === 'flat' && member.path) return path.basename(member.path, '.md');
510
+ return lowerCompact(member?.name || '');
511
+ }
512
+
513
+ function isFleetMember(member) {
514
+ if (!member || member.format !== 'directory') return false;
515
+ const slug = memberSlugFromRecord(member);
516
+ return Boolean(slug) && !new Set(['_archive', '_archived', '_template']).has(slug);
517
+ }
518
+
519
+ function missionTimeMs(mission) {
520
+ return Date.parse(mission?.last_tick_at || mission?.updated_at || mission?.created_at || '') || 0;
521
+ }
522
+
523
+ function memberOwnedMissions(name, missionMap = memberRunMissionMap()) {
524
+ const owner = lowerCompact(name);
525
+ return Array.from(missionMap.values())
526
+ .filter((mission) => lowerCompact(mission?.owner) === owner)
527
+ .sort((a, b) => missionTimeMs(b) - missionTimeMs(a));
528
+ }
529
+
530
+ function memberLatestMission(name, missionMap = memberRunMissionMap()) {
531
+ return memberOwnedMissions(name, missionMap)[0] || null;
532
+ }
533
+
534
+ function memberLiveMission(name, missionMap = memberRunMissionMap()) {
535
+ return memberOwnedMissions(name, missionMap)
536
+ .find((mission) => MEMBER_LIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) || null;
537
+ }
538
+
539
+ function missionShortName(mission, max = 36) {
540
+ return compactSentence(mission?.slug || mission?.title || mission?.objective || mission?.id || '', max) || '-';
541
+ }
542
+
543
+ function ageLabelFromMs(ms, nowMs = Date.now()) {
544
+ if (!ms) return '-';
545
+ const seconds = Math.max(0, Math.floor((nowMs - ms) / 1000));
546
+ if (seconds < 60) return 'now';
547
+ const minutes = Math.floor(seconds / 60);
548
+ if (minutes < 60) return `${minutes}m`;
549
+ const hours = Math.floor(minutes / 60);
550
+ if (hours < 48) return `${hours}h`;
551
+ return `${Math.floor(hours / 24)}d`;
552
+ }
553
+
554
+ function timestampMs(value) {
555
+ const ms = Date.parse(value || '');
556
+ return Number.isFinite(ms) ? ms : 0;
557
+ }
558
+
559
+ function newestMtimeRecursive(root) {
560
+ if (!fs.existsSync(root)) return 0;
561
+ let newest = 0;
562
+ const stack = [root];
563
+ while (stack.length) {
564
+ const current = stack.pop();
565
+ let stat = null;
566
+ try {
567
+ stat = fs.statSync(current);
568
+ } catch {
569
+ continue;
570
+ }
571
+ newest = Math.max(newest, stat.mtimeMs || 0);
572
+ if (!stat.isDirectory()) continue;
573
+ let entries = [];
574
+ try {
575
+ entries = fs.readdirSync(current);
576
+ } catch {
577
+ continue;
578
+ }
579
+ for (const entry of entries) stack.push(path.join(current, entry));
580
+ }
581
+ return newest;
582
+ }
583
+
584
+ function newestRunActivityMtime(name, root = process.cwd()) {
585
+ const runsDir = path.join(root, 'atris', 'runs');
586
+ if (!fs.existsSync(runsDir)) return 0;
587
+ const needle = lowerCompact(name);
588
+ let newest = 0;
589
+ for (const entry of fs.readdirSync(runsDir)) {
590
+ if (!lowerCompact(entry).includes(needle)) continue;
591
+ try {
592
+ newest = Math.max(newest, fs.statSync(path.join(runsDir, entry)).mtimeMs || 0);
593
+ } catch { /* ignore unreadable activity entries */ }
594
+ }
595
+ return newest;
596
+ }
597
+
598
+ function memberLastActivity(name, paths = memberPaths(name), root = process.cwd()) {
599
+ const logsMs = newestMtimeRecursive(path.join(paths.memberDir, 'logs'));
600
+ const runsMs = newestRunActivityMtime(name, root);
601
+ const ms = Math.max(logsMs, runsMs);
602
+ return {
603
+ at_ms: ms,
604
+ age: ageLabelFromMs(ms),
605
+ };
606
+ }
607
+
608
+ function memberMissionSummary(name, missionMap = memberRunMissionMap()) {
609
+ const latest = memberLatestMission(name, missionMap);
610
+ const live = memberLiveMission(name, missionMap);
611
+ const lastTickMs = timestampMs(latest?.last_tick_at);
612
+ return {
613
+ latest,
614
+ live,
615
+ name: missionShortName(latest),
616
+ state: latest?.status || '-',
617
+ last_tick_at: latest?.last_tick_at || null,
618
+ last_tick_age: ageLabelFromMs(lastTickMs),
619
+ live_mission: Boolean(live),
620
+ };
621
+ }
622
+
623
+ function memberVerdict({ mission, activity }, nowMs = Date.now()) {
624
+ const activityAge = activity?.at_ms ? nowMs - activity.at_ms : Number.POSITIVE_INFINITY;
625
+ if (mission?.live_mission || activityAge < MEMBER_ACTIVE_ACTIVITY_MS) return 'ACTIVE';
626
+ if (activityAge < MEMBER_RECENT_ACTIVITY_MS) return 'RECENT';
627
+ return 'IDLE';
628
+ }
629
+
630
+ function memberGoalPlaneStatus(name, paths = requireMemberDir(name)) {
631
+ const state = loadMemberGoals(name, paths);
632
+ const goal = activeGoal(state);
633
+ const current = memberOpenExperiment(state);
634
+ const lastReviewed = memberLastReviewedExperiment(state);
635
+ const value = memberValueSummary(state);
636
+ const logs = recentLogLines(paths.memberDir);
637
+ const needsUser = current?.status === 'blocked';
638
+ const stateLabel = !goal
639
+ ? 'no_goal'
640
+ : needsUser
641
+ ? 'needs_user'
642
+ : current
643
+ ? current.status
644
+ : 'ready';
645
+ const ask = needsUser ? (current.block?.ask || 'Needs operator input.') : null;
646
+ const nextCommand = !goal
647
+ ? `atris member goal ${name} "..."`
648
+ : needsUser
649
+ ? `atris member review ${name} ${current.id} --discard --proof "..."`
650
+ : current
651
+ ? `atris member review ${name} ${current.id} --accept --proof "..." --value 4`
652
+ : `atris member tick ${name}`;
653
+ return {
654
+ state,
655
+ goal,
656
+ current,
657
+ lastReviewed,
658
+ value,
659
+ logs,
660
+ needsUser,
661
+ stateLabel,
662
+ ask,
663
+ nextCommand,
664
+ };
665
+ }
666
+
443
667
  function resolveMemberRunMissionId(name, args = []) {
444
668
  const override = readFlag(args, '--mission', '') || readFlag(args, '--mission-id', '');
445
669
  if (override) return override;
@@ -450,8 +674,10 @@ function resolveMemberRunMissionId(name, args = []) {
450
674
  const runtimeId = memberRunRunnableMissionId(purpose.runtimeMission?.id, missionMap);
451
675
  if (runtimeId) return runtimeId;
452
676
 
677
+ // strictly active: activeGoal()'s goals[0] fallback would resurrect a
678
+ // paused goal's stale mission pointer and error the leg every hour
453
679
  const goals = loadMemberGoals(name, paths);
454
- const goal = activeGoal(goals);
680
+ const goal = (goals.goals || []).find((candidate) => candidate.status === 'active') || null;
455
681
  const goalId = memberRunRunnableMissionId(goal?.mission_id, missionMap);
456
682
  if (goalId) return goalId;
457
683
 
@@ -482,6 +708,7 @@ const MEMBER_RUN_START_VALUE_FLAGS = [
482
708
 
483
709
  const MEMBER_RUN_START_BOOLEAN_FLAGS = [
484
710
  '--json',
711
+ '--no-verify',
485
712
  '--worktree',
486
713
  '--shared-checkout',
487
714
  '--no-worktree',
@@ -517,16 +744,34 @@ function memberRunTruthRule(args = []) {
517
744
  || 'say what changed, what was checked, and what is still unproven';
518
745
  }
519
746
 
747
+ function operatorWaitingWishTexts(root = process.cwd()) {
748
+ // Wishes waiting on operator answers are not actionable candidates: seeding
749
+ // them into member runs spawned three parallel verifier-less wrapper
750
+ // missions chewing the same unanswered sentence (seen live 2026-07-07).
751
+ try {
752
+ const { waitingOperatorWishes } = require('../lib/wish-delegate');
753
+ return new Set(
754
+ waitingOperatorWishes(root)
755
+ .map((wish) => cleanMemberRunPhrase(wish?.text || wish?.task_text || '').toLowerCase())
756
+ .filter(Boolean),
757
+ );
758
+ } catch {
759
+ return new Set();
760
+ }
761
+ }
762
+
520
763
  function memberRunUsefulTarget() {
521
764
  try {
522
765
  const { nextMoves: pickNextMoves, isGenericInboxPlaceholder } = require('../lib/next-moves');
523
766
  const moves = pickNextMoves(process.cwd(), 5);
767
+ const waitingWishes = operatorWaitingWishTexts();
524
768
  const candidate = moves.find((move) => {
525
769
  const title = cleanMemberRunPhrase(move?.title);
526
770
  if (!title) return false;
527
771
  if (isGenericInboxPlaceholder(title)) return false;
528
772
  if (/^mission xp\s*:/i.test(title)) return false;
529
773
  if (/^decide and start the next useful mission after:/i.test(title)) return false;
774
+ if (waitingWishes.has(title.toLowerCase())) return false;
530
775
  return true;
531
776
  });
532
777
  return candidate?.title ? cleanMemberRunPhrase(candidate.title) : '';
@@ -569,17 +814,18 @@ function pushFlagWhenPresent(out, args, name) {
569
814
  if (hasFlag(args, name)) out.push(name);
570
815
  }
571
816
 
572
- function startMemberRunMission(name, missionText, args = []) {
573
- const paths = requireMemberDir(name);
574
- const owner = paths.storageName || name;
817
+ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cwd()) {
575
818
  const startArgs = [
576
819
  'mission',
577
820
  'start',
578
821
  missionText,
579
822
  '--owner',
580
823
  owner,
824
+ // codex_goal stalls unattended (no live codex session drives its native goal
825
+ // slot). Default to claude unless a live codex session is signalled; explicit
826
+ // --runner still wins. Proven footgun 2026-07-16. See lib/default-runner.js.
581
827
  '--runner',
582
- readFlag(args, '--runner', 'codex_goal'),
828
+ readFlag(args, '--runner', defaultObjectiveRunner(args)),
583
829
  '--lane',
584
830
  readFlag(args, '--lane', 'workspace'),
585
831
  ];
@@ -591,14 +837,43 @@ function startMemberRunMission(name, missionText, args = []) {
591
837
  pushFlagValue(startArgs, args, '--minutes');
592
838
  pushFlagValue(startArgs, args, '--hours');
593
839
  pushFlagValue(startArgs, args, '--base');
840
+ // A member-run mission is driven immediately by its runner, not parked on
841
+ // the queue — so the mission-start verifier gate (built for parked planning
842
+ // wishes) would only kill autonomous legs like autopilot's "member chooses
843
+ // useful work". When the caller named no verifier, opt out explicitly.
844
+ if (hasFlag(args, '--no-verify') || !readFlag(args, '--verify', '')) {
845
+ startArgs.push('--no-verify');
846
+ }
594
847
  pushFlagWhenPresent(startArgs, args, '--always-on');
595
848
  pushFlagWhenPresent(startArgs, args, '--xp-task');
596
849
  pushFlagWhenPresent(startArgs, args, '--agent-xp');
597
850
  pushFlagWhenPresent(startArgs, args, '--spend-full-budget');
598
851
  pushFlagWhenPresent(startArgs, args, '--use-whole-budget');
599
852
  pushFlagWhenPresent(startArgs, args, '--stop-when-done');
600
- if (!hasFlag(args, '--shared-checkout') && !hasFlag(args, '--no-worktree')) startArgs.push('--worktree');
853
+ // Default to an isolated worktree only when there is a git repo to cut it
854
+ // from. A plain (non-git) workspace would fail every mission start with
855
+ // "fatal: not a git repository" — degrade to the shared checkout instead.
856
+ if (!hasFlag(args, '--shared-checkout') && !hasFlag(args, '--no-worktree') && insideGitRepo(cwd)) {
857
+ startArgs.push('--worktree');
858
+ }
601
859
  if (hasFlag(args, '--json')) startArgs.push('--json');
860
+ return startArgs;
861
+ }
862
+
863
+ function insideGitRepo(startDir) {
864
+ let dir = path.resolve(startDir || process.cwd());
865
+ while (true) {
866
+ if (fs.existsSync(path.join(dir, '.git'))) return true;
867
+ const parent = path.dirname(dir);
868
+ if (parent === dir) return false;
869
+ dir = parent;
870
+ }
871
+ }
872
+
873
+ function startMemberRunMission(name, missionText, args = []) {
874
+ const paths = requireMemberDir(name);
875
+ const owner = paths.storageName || name;
876
+ const startArgs = buildMemberRunStartArgs(owner, missionText, args);
602
877
 
603
878
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
604
879
  try {
@@ -1439,14 +1714,43 @@ function readRecentWakeReceiptEvidence(name) {
1439
1714
  }
1440
1715
 
1441
1716
  function readJsonlRowsIfExists(filePath, maxRows = 80) {
1717
+ const tailBytes = 256 * 1024;
1718
+ let fd;
1442
1719
  let text = '';
1720
+ let truncated = false;
1443
1721
  try {
1444
- text = fs.readFileSync(filePath, 'utf8');
1722
+ fd = fs.openSync(filePath, 'r');
1723
+ const stat = fs.fstatSync(fd);
1724
+ const bytesToRead = Math.min(stat.size, tailBytes);
1725
+ const start = Math.max(0, stat.size - bytesToRead);
1726
+ const buffer = Buffer.alloc(bytesToRead);
1727
+ let bytesRead = 0;
1728
+ while (bytesRead < bytesToRead) {
1729
+ const count = fs.readSync(
1730
+ fd,
1731
+ buffer,
1732
+ bytesRead,
1733
+ bytesToRead - bytesRead,
1734
+ start + bytesRead
1735
+ );
1736
+ if (count === 0) break;
1737
+ bytesRead += count;
1738
+ }
1739
+ text = buffer.toString('utf8', 0, bytesRead);
1740
+ truncated = start > 0;
1445
1741
  } catch {
1446
1742
  return [];
1743
+ } finally {
1744
+ if (fd !== undefined) {
1745
+ try { fs.closeSync(fd); } catch {}
1746
+ }
1447
1747
  }
1448
1748
  const rows = [];
1449
- const lines = text.split(/\r?\n/).filter((line) => line.trim()).slice(-Math.max(1, maxRows));
1749
+ const lines = text
1750
+ .split(/\r?\n/)
1751
+ .slice(truncated ? 1 : 0)
1752
+ .filter((line) => line.trim())
1753
+ .slice(-Math.max(1, maxRows));
1450
1754
  for (const line of lines) {
1451
1755
  try {
1452
1756
  rows.push(JSON.parse(line));
@@ -2706,6 +3010,17 @@ function failureCoveredByPassLesson(line, passLessonText = '') {
2706
3010
  return /spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(passLessonText)
2707
3011
  && (!target || passLessonText.includes(target) || (targetTail && passLessonText.includes(targetTail)));
2708
3012
  }
3013
+ // General remediation channel: a pass lesson that quotes the failure's
3014
+ // normalized pattern marks it fixed. Old log lines stay on disk but stop
3015
+ // feeding the pain score, so pain can actually go down after a fix.
3016
+ const pattern = normalizeFailurePattern(text);
3017
+ const bare = pattern.replace(/^#+\s*[#:.]*\s*·\s*/, '').trim();
3018
+ for (const candidate of [pattern, bare]) {
3019
+ if (candidate && candidate.length >= 12
3020
+ && passLessonText.toLowerCase().includes(candidate.toLowerCase())) {
3021
+ return true;
3022
+ }
3023
+ }
2709
3024
  return false;
2710
3025
  }
2711
3026
 
@@ -3540,8 +3855,8 @@ function findAllMembers(teamDir) {
3540
3855
  const entries = fs.readdirSync(teamDir);
3541
3856
 
3542
3857
  for (const entry of entries) {
3543
- // Skip template directory and hidden files
3544
- if (entry === '_template' || entry.startsWith('.')) continue;
3858
+ // Skip template/archive directories and hidden files
3859
+ if (entry === '_template' || entry === '_archive' || entry === '_archived' || entry.startsWith('.')) continue;
3545
3860
 
3546
3861
  const fullPath = path.join(teamDir, entry);
3547
3862
  const stat = fs.statSync(fullPath);
@@ -3598,6 +3913,7 @@ function findAllMembers(teamDir) {
3598
3913
  const content = fs.readFileSync(fullPath, 'utf8');
3599
3914
  const fm = parseFrontmatter(content);
3600
3915
  if (!fm) continue; // No frontmatter = not a member
3916
+ if (fm.type && fm.type !== 'member') continue; // Index/doc files (e.g. ASSIGNMENTS.md, type: index) are not members
3601
3917
 
3602
3918
  const name = entry.replace('.md', '');
3603
3919
  members.push({
@@ -6876,6 +7192,7 @@ function readMemberScope(name, paths) {
6876
7192
  function isAutoGeneratedArtifact(filePath, name) {
6877
7193
  const memberLogs = `atris/team/${name}/logs/`;
6878
7194
  if (filePath.startsWith(memberLogs)) return true;
7195
+ if (filePath === `atris/team/${name}/goals.json`) return true;
6879
7196
  if (filePath === `atris/team/${name}/now.md`) return true;
6880
7197
  return false;
6881
7198
  }
@@ -7404,6 +7721,127 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
7404
7721
  };
7405
7722
  }
7406
7723
 
7724
+ // --- Wake boot rendering: the human face of `gm <member>` ---
7725
+ // The JSON contract (atris.member_wake.v1) is untouched; this only changes what a person sees.
7726
+
7727
+ const WAKE_REASON_TEXT = {
7728
+ mission_missing_or_placeholder: "it has no North Star yet, and it won't guess at important work",
7729
+ no_active_goal: 'it has no active goal to push on yet',
7730
+ execute_requires_confirm_autonomy_policy: 'autonomous execution waits for your explicit go-ahead',
7731
+ workspace_dirty_in_member_scope: "there are uncommitted changes in its own lane — it won't build on a dirty floor",
7732
+ blocked_experiment: 'its current experiment is blocked on a human answer',
7733
+ tick_executed_experiment_proposed: 'it proposed one bounded experiment and queued it for your review',
7734
+ safe_next_bounded_step: 'it found a safe bounded next step',
7735
+ loop_already_active: 'another loop already holds the lease, so it stepped back instead of double-working',
7736
+ member_archived: 'this member is archived',
7737
+ llm_not_configured: 'no model is configured for autonomous reasoning here',
7738
+ insufficient_data: "it doesn't have enough evidence yet to act with confidence",
7739
+ proof_not_successful: 'the last proof did not pass, so it stopped instead of stacking work on a failure',
7740
+ auto_improver_task_create_failed: 'it found an improvement but could not put the task on the board',
7741
+ heuristic_cross_domain_proof_written: 'it wrote a cross-domain proof using its built-in heuristics',
7742
+ heuristic_objective_proposal_written: 'it drafted an objective proposal using its built-in heuristics',
7743
+ install_requires_clean_git: 'installing needs a clean git tree first',
7744
+ insufficient_world_model_data: 'its world model is too thin to act on yet',
7745
+ llm_json_parse_failed: 'the model reply did not parse, so it stopped rather than act on garbage',
7746
+ llm_json_parse_failed_heuristic_used: 'the model reply did not parse, so it fell back to built-in heuristics',
7747
+ missing_domain_input: 'it needs a domain file or domain text from you to work on',
7748
+ no_crontab: 'no crontab is available on this machine, so the loop cannot be scheduled',
7749
+ };
7750
+
7751
+ const WAKE_DECISION_TEXT = {
7752
+ ask: 'needs one thing from you',
7753
+ wait: 'decided to hold',
7754
+ tick: 'picked one bounded step',
7755
+ close_loop: 'is closing the loop',
7756
+ report_proof: 'has proof ready for you',
7757
+ create_missing_task: 'is putting the missing task on the board',
7758
+ create_task: 'is putting a new task on the board',
7759
+ stop: 'stopped on purpose',
7760
+ task_create_failed: 'tried to put a task on the board and could not',
7761
+ set_objective: 'set its own objective',
7762
+ generate_objective: 'drafted a new objective for review',
7763
+ supervise: 'reviewed the rest of the team',
7764
+ wiki_mine: 'is mining the wiki for its next move',
7765
+ process_domain_file: 'is digesting the domain file you gave it',
7766
+ cross_domain_generalize: 'is distilling patterns across domains',
7767
+ };
7768
+
7769
+ function wakeDecisionText(decision) {
7770
+ if (WAKE_DECISION_TEXT[decision]) return WAKE_DECISION_TEXT[decision];
7771
+ const words = String(decision || '').replace(/_/g, ' ').trim();
7772
+ return words ? `decided: ${words}` : 'decided nothing';
7773
+ }
7774
+
7775
+ function wakeStyle() {
7776
+ const on = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
7777
+ const wrap = (code) => (text) => (on ? `\x1b[${code}m${text}\x1b[0m` : String(text));
7778
+ return { bold: wrap('1'), dim: wrap('2'), cyan: wrap('36') };
7779
+ }
7780
+
7781
+ function wakeMemberRole(name) {
7782
+ try {
7783
+ const content = fs.readFileSync(memberPaths(name).memberFile, 'utf8');
7784
+ const fm = parseFrontmatter(content);
7785
+ return (fm && fm.role) || null;
7786
+ } catch {
7787
+ return null;
7788
+ }
7789
+ }
7790
+
7791
+ function wakeReasonText(reason) {
7792
+ if (WAKE_REASON_TEXT[reason]) return WAKE_REASON_TEXT[reason];
7793
+ if (reason === 'open_experiment_proposed') return 'it already has an experiment on the table, waiting for your review';
7794
+ if (reason === 'open_experiment_running') return 'its current experiment is still running';
7795
+ const openExperiment = String(reason || '').match(/^open_experiment_([a-z_]+)$/);
7796
+ if (openExperiment) return `its current experiment is ${openExperiment[1].replace(/_/g, ' ')}`;
7797
+ return String(reason || '').replace(/_/g, ' ');
7798
+ }
7799
+
7800
+ function wakeBootLines(name, result) {
7801
+ const s = wakeStyle();
7802
+ const role = wakeMemberRole(name);
7803
+ const decisionText = wakeDecisionText(result.decision);
7804
+ const goal = result.active_goal && result.active_goal.title;
7805
+ const experiment = result.current_experiment && result.current_experiment.title;
7806
+ const northStar = result.mission && result.mission.north_star;
7807
+ const laneCandidates = result.evidence && result.evidence.task_projection
7808
+ ? result.evidence.task_projection.candidate_count
7809
+ : null;
7810
+
7811
+ const lines = [
7812
+ '',
7813
+ `${s.cyan('\u25c8')} ${s.bold(name)} is waking up${role ? ` ${s.dim(`\u00b7 ${role}`)}` : ''}`,
7814
+ '',
7815
+ ` mission ${northStar ? clipText(northStar, 70) : s.dim('no North Star yet')}`,
7816
+ ` goal ${goal ? clipText(goal, 70) : s.dim('none yet')}`,
7817
+ ...(experiment ? [` working ${clipText(experiment, 70)}`] : []),
7818
+ ...(laneCandidates !== null ? [` lane ${laneCandidates} task${laneCandidates === 1 ? '' : 's'} waiting in ${name}'s lane`] : []),
7819
+ '',
7820
+ ` ${s.bold(name)} looked around and ${s.bold(decisionText)}.`,
7821
+ ` ${s.dim(`Why: ${wakeReasonText(result.reason)}.`)}`,
7822
+ ];
7823
+
7824
+ if (result.created_task && result.created_task.title) {
7825
+ lines.push('', ` On the board: ${result.created_task.display_id || result.created_task.legacy_ref || ''} ${clipText(result.created_task.title, 70)}`.trimEnd());
7826
+ }
7827
+ if (result.ask) {
7828
+ lines.push('', ` ${s.bold('One thing from you:')}`, ` ${result.ask}`);
7829
+ }
7830
+ if (result.next_command) {
7831
+ const ballWithHuman = result.needs_user === true
7832
+ || result.decision === 'ask'
7833
+ || /^open_experiment_/.test(result.reason || '');
7834
+ lines.push('', ` ${s.bold(ballWithHuman ? 'Your move' : 'Next step')}`, ` ${s.cyan(result.next_command)}`);
7835
+ }
7836
+ lines.push('', ` ${s.dim(`receipt \u00b7 ${path.relative(process.cwd(), result.receipt_path)}`)}`, '');
7837
+ return lines;
7838
+ }
7839
+
7840
+ function clipText(value, max = 70) {
7841
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
7842
+ return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1)).trim()}\u2026`;
7843
+ }
7844
+
7407
7845
  async function memberWake(name, ...args) {
7408
7846
  const asJson = hasFlag(args, '--json');
7409
7847
  const execute = hasFlag(args, '--execute') && !hasFlag(args, '--dry-run');
@@ -7414,28 +7852,127 @@ async function memberWake(name, ...args) {
7414
7852
  file: readFlag(args, '--domain-file', ''),
7415
7853
  name: readFlag(args, '--domain-name', ''),
7416
7854
  };
7855
+ // Honor per-member sleep switch before any tick dispatch.
7856
+ try {
7857
+ const { isMemberAwake } = require('../lib/member-switches');
7858
+ if (name && !isMemberAwake(name)) {
7859
+ const skipped = {
7860
+ ok: true,
7861
+ skipped: true,
7862
+ reason: 'asleep',
7863
+ member: name,
7864
+ decision: 'stop',
7865
+ };
7866
+ printJsonOrText(skipped, [`${name} is asleep`], asJson);
7867
+ return skipped;
7868
+ }
7869
+ } catch {
7870
+ // switch store unreadable: fail open and continue wake
7871
+ }
7417
7872
  const result = await runMemberWake(name, { execute, confirmed, force, domainInput });
7418
- printJsonOrText(
7419
- result,
7420
- [
7421
- `Wake: ${name}`,
7422
- `Decision: ${result.decision}`,
7423
- `Reason: ${result.reason}`,
7424
- `Mode: ${result.mode}${result.executed ? ' executed' : ''}`,
7425
- ...(result.ask ? [`Ask: ${result.ask}`] : []),
7426
- `Next: ${result.next_command}`,
7427
- `Receipt: ${path.relative(process.cwd(), result.receipt_path)}`,
7428
- ],
7429
- asJson,
7430
- );
7873
+ printJsonOrText(result, wakeBootLines(name, result), asJson);
7431
7874
  }
7432
7875
 
7433
7876
  function memberAlive(name, ...args) {
7877
+ if (name === '--all' || name === 'all') return memberAliveAll(...args);
7434
7878
  const nextArgs = args.includes('--alive') ? args : [...args, '--alive'];
7435
7879
  return memberLoop(name, ...nextArgs);
7436
7880
  }
7437
7881
 
7882
+ function collectFleetActivationRows(root = process.cwd()) {
7883
+ const teamDir = path.join(root, 'atris', 'team');
7884
+ const missionMap = memberRunMissionMap();
7885
+ return findAllMembers(teamDir)
7886
+ .filter(isFleetMember)
7887
+ .map((member) => {
7888
+ const slug = memberSlugFromRecord(member);
7889
+ const paths = memberPaths(slug);
7890
+ const goalPlane = memberGoalPlaneStatus(slug, paths);
7891
+ const activeMission = memberLiveMission(slug, missionMap);
7892
+ const latestMission = memberLatestMission(slug, missionMap);
7893
+ const skipped = !goalPlane.goal && !activeMission;
7894
+ return {
7895
+ member: slug,
7896
+ goal: goalPlane.goal || null,
7897
+ active_mission: activeMission || null,
7898
+ latest_mission: latestMission || null,
7899
+ skipped,
7900
+ skip_reason: skipped ? 'no_goal_and_no_active_mission' : null,
7901
+ };
7902
+ })
7903
+ .sort((a, b) => a.member.localeCompare(b.member));
7904
+ }
7905
+
7906
+ async function callMemberLoopForFleet(kind, member, args, captureOutput) {
7907
+ if (!captureOutput) {
7908
+ if (kind === 'alive') return memberAlive(member, ...args);
7909
+ return memberLoop(member, ...args);
7910
+ }
7911
+ const captured = [];
7912
+ const originalLog = console.log;
7913
+ console.log = (...parts) => captured.push(parts.join(' '));
7914
+ try {
7915
+ const result = kind === 'alive'
7916
+ ? await memberAlive(member, ...args)
7917
+ : await memberLoop(member, ...args);
7918
+ return { result, output: captured };
7919
+ } finally {
7920
+ console.log = originalLog;
7921
+ }
7922
+ }
7923
+
7924
+ async function memberLoopAll(kind, ...args) {
7925
+ const asJson = hasFlag(args, '--json');
7926
+ const rows = collectFleetActivationRows();
7927
+ const activated = [];
7928
+ const skipped = [];
7929
+ if (!asJson) console.log(`Member fleet ${kind}`);
7930
+ for (const row of rows) {
7931
+ if (row.skipped) {
7932
+ skipped.push(row);
7933
+ if (!asJson) console.log(`Skip ${row.member}: no goal and no active mission.`);
7934
+ continue;
7935
+ }
7936
+ if (!asJson) console.log(`Activate ${row.member}: ${row.goal ? 'goal' : `mission ${row.active_mission?.id || 'active'}`}`);
7937
+ const result = await callMemberLoopForFleet(kind, row.member, args, asJson);
7938
+ activated.push({
7939
+ member: row.member,
7940
+ reason: row.goal ? 'has_goal' : 'has_active_mission',
7941
+ mission_id: row.active_mission?.id || null,
7942
+ result: asJson ? result?.result || null : null,
7943
+ output: asJson ? result?.output || [] : undefined,
7944
+ });
7945
+ }
7946
+ const reasonCounts = skipped.reduce((acc, row) => {
7947
+ acc[row.skip_reason] = (acc[row.skip_reason] || 0) + 1;
7948
+ return acc;
7949
+ }, {});
7950
+ const payload = {
7951
+ ok: true,
7952
+ action: `${kind}_all`,
7953
+ activated: activated.length,
7954
+ skipped: skipped.length,
7955
+ skipped_reasons: reasonCounts,
7956
+ activated_members: activated,
7957
+ skipped_members: skipped.map((row) => ({
7958
+ member: row.member,
7959
+ reason: row.skip_reason,
7960
+ latest_mission_id: row.latest_mission?.id || null,
7961
+ latest_mission_status: row.latest_mission?.status || null,
7962
+ })),
7963
+ };
7964
+ printJsonOrText(payload, [
7965
+ `Summary: ${activated.length} activated, ${skipped.length} skipped (${Object.entries(reasonCounts).map(([reason, count]) => `${reason} x${count}`).join(', ') || 'none'}).`,
7966
+ ], asJson);
7967
+ return payload;
7968
+ }
7969
+
7970
+ function memberAliveAll(...args) {
7971
+ return memberLoopAll('alive', ...args);
7972
+ }
7973
+
7438
7974
  async function memberLoop(name, ...args) {
7975
+ if (name === '--all' || name === 'all') return memberLoopAll('loop', ...args);
7439
7976
  requireMemberDir(name);
7440
7977
  const asJson = hasFlag(args, '--json');
7441
7978
  const aliveMode = hasFlag(args, '--alive');
@@ -7615,6 +8152,13 @@ async function memberLoop(name, ...args) {
7615
8152
  let earlyExit = null;
7616
8153
  let consecutiveIdle = 0;
7617
8154
  const idleBreakThreshold = 2;
8155
+ // Repeated identical `ask:*` wake decisions (e.g. ask:mission_missing_or_placeholder) mean the
8156
+ // member is stuck asking the same question every tick regardless of --execute; the idle
8157
+ // early-exit above only fires in execute mode, so without this a dry-run loop burns every
8158
+ // remaining tick re-asking. Reuse the same blocked_on_human handoff once we see the ask repeat.
8159
+ let lastAskKey = null;
8160
+ let consecutiveIdenticalAsk = 0;
8161
+ const identicalAskBreakThreshold = 2;
7618
8162
 
7619
8163
  try {
7620
8164
  for (let index = 0; index < ticks; index += 1) {
@@ -7686,6 +8230,29 @@ async function memberLoop(name, ...args) {
7686
8230
  };
7687
8231
  tickResults.push(tick);
7688
8232
  fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
8233
+
8234
+ if (wake.decision === 'ask') {
8235
+ if (key === lastAskKey) {
8236
+ consecutiveIdenticalAsk += 1;
8237
+ } else {
8238
+ lastAskKey = key;
8239
+ consecutiveIdenticalAsk = 1;
8240
+ }
8241
+ if (consecutiveIdenticalAsk >= identicalAskBreakThreshold) {
8242
+ earlyExit = {
8243
+ after_tick: tick.tick,
8244
+ decision: tick.decision || null,
8245
+ reason: tick.reason || 'idle',
8246
+ needs_user: tick.needs_user === true,
8247
+ blocked_on_human: true,
8248
+ next_command: tick.next_command || null,
8249
+ stop: 'repeated_identical_ask',
8250
+ };
8251
+ }
8252
+ } else {
8253
+ lastAskKey = null;
8254
+ consecutiveIdenticalAsk = 0;
8255
+ }
7689
8256
  }
7690
8257
  } catch (error) {
7691
8258
  failed = true;
@@ -7700,6 +8267,7 @@ async function memberLoop(name, ...args) {
7700
8267
  fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
7701
8268
  break;
7702
8269
  }
8270
+ if (earlyExit) break;
7703
8271
  if (execute) {
7704
8272
  const last = tickResults[tickResults.length - 1];
7705
8273
  if (last && last.productive) {
@@ -7748,6 +8316,7 @@ async function memberLoop(name, ...args) {
7748
8316
  duration_ms_actual: Date.parse(finishedAt) - Date.parse(startedAt),
7749
8317
  decisions,
7750
8318
  early_exit: earlyExit,
8319
+ stop: earlyExit?.stop || null,
7751
8320
  blocked_on_human: earlyExit?.blocked_on_human === true,
7752
8321
  needs_user: earlyExit?.needs_user === true,
7753
8322
  next_command: earlyExit?.next_command || null,
@@ -7962,58 +8531,173 @@ function memberBlock(name, experimentId, ...args) {
7962
8531
  );
7963
8532
  }
7964
8533
 
8534
+ function collectFleetMemberStatus(root = process.cwd()) {
8535
+ const teamDir = path.join(root, 'atris', 'team');
8536
+ const missionMap = memberRunMissionMap();
8537
+ return findAllMembers(teamDir)
8538
+ .filter(isFleetMember)
8539
+ .map((member) => {
8540
+ const slug = memberSlugFromRecord(member);
8541
+ const paths = memberPaths(slug);
8542
+ const goalPlane = memberGoalPlaneStatus(slug, paths);
8543
+ const mission = memberMissionSummary(slug, missionMap);
8544
+ const activity = memberLastActivity(slug, paths, root);
8545
+ const verdict = memberVerdict({ mission, activity });
8546
+ return {
8547
+ member: slug,
8548
+ role: member.role,
8549
+ goal_state: goalPlane.stateLabel,
8550
+ goal_title: goalPlane.goal?.title || null,
8551
+ mission: mission.latest ? {
8552
+ id: mission.latest.id || null,
8553
+ name: mission.name,
8554
+ state: mission.state,
8555
+ last_tick_at: mission.last_tick_at,
8556
+ last_tick_age: mission.last_tick_age,
8557
+ live: mission.live_mission,
8558
+ } : null,
8559
+ last_activity: activity,
8560
+ verdict,
8561
+ };
8562
+ })
8563
+ .sort((a, b) => a.member.localeCompare(b.member));
8564
+ }
8565
+
8566
+ function clipCell(value, width) {
8567
+ const clean = String(value || '-').replace(/\s+/g, ' ').trim() || '-';
8568
+ if (clean.length <= width) return clean.padEnd(width);
8569
+ if (width <= 3) return clean.slice(0, width);
8570
+ return `${clean.slice(0, width - 3)}...`;
8571
+ }
8572
+
8573
+ function renderMemberFleetStatusTable(rows) {
8574
+ if (!rows.length) return ['No active team members found in atris/team/.'];
8575
+ const widths = {
8576
+ member: 18,
8577
+ goal: 11,
8578
+ mission: 34,
8579
+ state: 10,
8580
+ tick: 8,
8581
+ activity: 8,
8582
+ verdict: 8,
8583
+ };
8584
+ const header = [
8585
+ clipCell('Member', widths.member),
8586
+ clipCell('Goal', widths.goal),
8587
+ clipCell('Mission', widths.mission),
8588
+ clipCell('State', widths.state),
8589
+ clipCell('Tick', widths.tick),
8590
+ clipCell('Activity', widths.activity),
8591
+ clipCell('Verdict', widths.verdict),
8592
+ ].join(' ');
8593
+ const rule = [
8594
+ '-'.repeat(widths.member),
8595
+ '-'.repeat(widths.goal),
8596
+ '-'.repeat(widths.mission),
8597
+ '-'.repeat(widths.state),
8598
+ '-'.repeat(widths.tick),
8599
+ '-'.repeat(widths.activity),
8600
+ '-'.repeat(widths.verdict),
8601
+ ].join(' ');
8602
+ return [
8603
+ 'Member fleet status',
8604
+ header,
8605
+ rule,
8606
+ ...rows.map((row) => [
8607
+ clipCell(row.member, widths.member),
8608
+ clipCell(row.goal_state, widths.goal),
8609
+ clipCell(row.mission?.name || '-', widths.mission),
8610
+ clipCell(row.mission?.state || '-', widths.state),
8611
+ clipCell(row.mission?.last_tick_age || '-', widths.tick),
8612
+ clipCell(row.last_activity?.age || '-', widths.activity),
8613
+ clipCell(row.verdict, widths.verdict),
8614
+ ].join(' ')),
8615
+ ];
8616
+ }
8617
+
8618
+ function memberStatusAll(...args) {
8619
+ const asJson = hasFlag(args, '--json');
8620
+ const rows = collectFleetMemberStatus();
8621
+ printJsonOrText(
8622
+ {
8623
+ ok: true,
8624
+ action: 'status_all',
8625
+ members: rows,
8626
+ },
8627
+ renderMemberFleetStatusTable(rows),
8628
+ asJson,
8629
+ );
8630
+ }
8631
+
7965
8632
  function memberStatus(name, ...args) {
8633
+ if (name === '--all' || name === 'all') return memberStatusAll(...args);
7966
8634
  const paths = requireMemberDir(name);
7967
8635
  const asJson = hasFlag(args, '--json');
7968
- const state = loadMemberGoals(name, paths);
7969
- const goal = activeGoal(state);
7970
- const current = memberOpenExperiment(state);
7971
- const lastReviewed = memberLastReviewedExperiment(state);
7972
- const value = memberValueSummary(state);
7973
- const logs = recentLogLines(paths.memberDir);
7974
- const needsUser = current?.status === 'blocked';
7975
- const stateLabel = !goal
7976
- ? 'no_goal'
7977
- : needsUser
7978
- ? 'needs_user'
7979
- : current
7980
- ? current.status
7981
- : 'ready';
7982
- const ask = needsUser ? (current.block?.ask || 'Needs operator input.') : null;
7983
- const nextCommand = !goal
7984
- ? `atris member goal ${name} "..."`
7985
- : needsUser
7986
- ? `atris member review ${name} ${current.id} --discard --proof "..."`
7987
- : current
7988
- ? `atris member review ${name} ${current.id} --accept --proof "..." --value 4`
7989
- : `atris member tick ${name}`;
8636
+ const goalPlane = memberGoalPlaneStatus(name, paths);
8637
+ const owner = paths.storageName || name;
8638
+ const mission = memberMissionSummary(owner, memberRunMissionMap());
8639
+ const activity = memberLastActivity(owner, paths);
8640
+ const verdict = memberVerdict({ mission, activity });
8641
+ let memberSwitch = { awake: true, loops: {} };
8642
+ try {
8643
+ const { getMemberSwitch } = require('../lib/member-switches');
8644
+ memberSwitch = getMemberSwitch(name);
8645
+ } catch {
8646
+ // default awake
8647
+ }
8648
+ const loopEntries = Object.entries(memberSwitch.loops || {})
8649
+ .map(([id, on]) => `${id}:${on === false ? 'asleep' : 'awake'}`);
8650
+ const switchLabel = memberSwitch.awake === false ? 'asleep' : 'awake';
7990
8651
  const payload = {
7991
8652
  ok: true,
7992
8653
  action: 'status',
7993
8654
  member: name,
7994
- state: stateLabel,
7995
- needs_user: needsUser,
7996
- ask,
7997
- active_goal: goal || null,
7998
- current_experiment: current || null,
7999
- last_reviewed: lastReviewed || null,
8000
- value,
8001
- next_command: nextCommand,
8002
- recent_log: logs,
8655
+ state: goalPlane.stateLabel,
8656
+ needs_user: goalPlane.needsUser,
8657
+ ask: goalPlane.ask,
8658
+ active_goal: goalPlane.goal || null,
8659
+ current_experiment: goalPlane.current || null,
8660
+ last_reviewed: goalPlane.lastReviewed || null,
8661
+ value: goalPlane.value,
8662
+ switch: {
8663
+ awake: memberSwitch.awake !== false,
8664
+ loops: memberSwitch.loops || {},
8665
+ },
8666
+ mission: mission.latest ? {
8667
+ id: mission.latest.id || null,
8668
+ name: mission.name,
8669
+ state: mission.state,
8670
+ last_tick_at: mission.last_tick_at,
8671
+ last_tick_age: mission.last_tick_age,
8672
+ live: mission.live_mission,
8673
+ } : null,
8674
+ last_activity: activity,
8675
+ verdict,
8676
+ next_command: goalPlane.nextCommand,
8677
+ recent_log: goalPlane.logs,
8003
8678
  goals_path: paths.goalsJson,
8004
8679
  goals_md_path: paths.goalsMd,
8005
8680
  };
8681
+ const s = wakeStyle();
8682
+ const role = wakeMemberRole(name);
8683
+ const ballWithHuman = goalPlane.needsUser === true || goalPlane.current?.status === 'proposed';
8006
8684
  printJsonOrText(
8007
8685
  payload,
8008
8686
  [
8009
- `Member: ${name}`,
8010
- `State: ${stateLabel}`,
8011
- `Goal: ${goal?.title || 'No goal yet'}`,
8012
- `Current: ${current ? `${current.status} - ${current.title}` : 'No open experiment'}`,
8013
- ...(ask ? [`Ask: ${ask}`] : []),
8014
- `Value: ${value.line}`,
8015
- `Next: ${nextCommand}`,
8016
- ...(logs.length ? ['Recent log:', ...logs.map((line) => ` ${line}`)] : []),
8687
+ '',
8688
+ `${s.cyan('\u25c8')} ${s.bold(name)}${role ? ` ${s.dim(`\u00b7 ${role}`)}` : ''} ${s.dim(`\u00b7 ${verdict.toLowerCase()}, last activity ${activity.age}`)}`,
8689
+ '',
8690
+ ` goal ${goalPlane.goal?.title ? clipText(goalPlane.goal.title, 70) : s.dim('none yet')}`,
8691
+ ` working ${goalPlane.current ? `${clipText(goalPlane.current.title, 60)} ${s.dim(`(${goalPlane.current.status})`)}` : s.dim('no open experiment')}`,
8692
+ ` value ${goalPlane.value.line}`,
8693
+ ` switch ${switchLabel}${loopEntries.length ? ` ${s.dim(`(${loopEntries.join(', ')})`)}` : ''}`,
8694
+ ` mission ${mission.latest ? `${mission.name} ${s.dim(`(${mission.state}, last tick ${mission.last_tick_age})`)}` : s.dim('none owned')}`,
8695
+ ...(goalPlane.ask ? ['', ` ${s.bold('One thing from you:')}`, ` ${goalPlane.ask}`] : []),
8696
+ '',
8697
+ ` ${s.bold(ballWithHuman ? 'Your move' : 'Next step')}`,
8698
+ ` ${s.cyan(goalPlane.nextCommand)}`,
8699
+ ...(goalPlane.logs.length ? ['', ` ${s.dim('recent')}`, ...goalPlane.logs.map((line) => ` ${s.dim(line)}`)] : []),
8700
+ '',
8017
8701
  ],
8018
8702
  asJson,
8019
8703
  );
@@ -8107,6 +8791,143 @@ function memberHistory(name, ...args) {
8107
8791
  }
8108
8792
  }
8109
8793
 
8794
+ // --- Live backend status (heartbeat + projection) ---
8795
+
8796
+ function parseLiveFlags(args) {
8797
+ const flags = { scope: 'personal', scopeId: null, json: false, state: null, note: null, days: null };
8798
+ const positional = [];
8799
+ for (let i = 0; i < args.length; i++) {
8800
+ const a = args[i];
8801
+ if (a === '--json') flags.json = true;
8802
+ else if (a === '--done') flags.state = 'done';
8803
+ else if (a === '--error') flags.state = 'error';
8804
+ else if (a === '--running') flags.state = 'running';
8805
+ else if (a === '--scope') flags.scope = args[++i];
8806
+ else if (a.startsWith('--scope=')) flags.scope = a.slice(8);
8807
+ else if (a === '--scope-id') flags.scopeId = args[++i];
8808
+ else if (a.startsWith('--scope-id=')) flags.scopeId = a.slice(11);
8809
+ else if (a === '--note' || a === '--on') flags.note = args[++i];
8810
+ else if (a.startsWith('--note=')) flags.note = a.slice(7);
8811
+ else if (a === '--days') flags.days = parseInt(args[++i], 10);
8812
+ else if (a.startsWith('--days=')) flags.days = parseInt(a.slice(7), 10);
8813
+ else positional.push(a);
8814
+ }
8815
+ return { flags, positional };
8816
+ }
8817
+
8818
+ function memberScopeQuery(flags) {
8819
+ let q = `scope=${encodeURIComponent(flags.scope)}`;
8820
+ if (flags.scopeId) q += `&scope_id=${encodeURIComponent(flags.scopeId)}`;
8821
+ return q;
8822
+ }
8823
+
8824
+ // atris member live — what the iPhone app and web see: GET /api/members projection.
8825
+ async function memberLive(...args) {
8826
+ const { flags } = parseLiveFlags(args);
8827
+ const creds = loadCredentials();
8828
+ if (!creds || !creds.token) {
8829
+ console.error('Not logged in. Run: atris login');
8830
+ process.exit(1);
8831
+ }
8832
+ const result = await apiRequestJson(`/members?${memberScopeQuery(flags)}`, { token: creds.token });
8833
+ if (!result.ok) {
8834
+ console.error(`Failed to fetch members: ${result.error || 'Unknown error'}`);
8835
+ process.exit(1);
8836
+ }
8837
+ const members = (result.data?.members || result.data || []).filter((m) => m && m.member_type === 'ai');
8838
+ if (flags.json) {
8839
+ console.log(JSON.stringify(members, null, 2));
8840
+ return;
8841
+ }
8842
+ console.log('');
8843
+ console.log(`live members (${flags.scope}${flags.scopeId ? `:${flags.scopeId}` : ''}) — same projection the app renders`);
8844
+ console.log('');
8845
+ for (const m of members) {
8846
+ const loop = m.loop_status || {};
8847
+ const dot = loop.running ? '●' : '○';
8848
+ const state = loop.state || m.status || '-';
8849
+ const workingOn = loop.working_on || loop.now || '';
8850
+ const lastActive = loop.last_active || '';
8851
+ console.log(` ${dot} ${m.display_name} [${state}]${workingOn ? ` ${workingOn}` : ''}${lastActive ? ` (last active ${lastActive})` : ''}`);
8852
+ }
8853
+ if (members.length === 0) console.log(' (no AI members in this scope)');
8854
+ console.log('');
8855
+ }
8856
+
8857
+ // atris member heartbeat — tell the backend this member is running/done, so
8858
+ // every surface (iOS, web) shows it live. POST /api/members/{name}/heartbeat.
8859
+ async function memberHeartbeat(name, ...args) {
8860
+ if (!name) {
8861
+ console.error('Usage: atris member heartbeat <name> [--note "what it is doing"] [--done|--error] [--scope business --scope-id <id>] [--json]');
8862
+ process.exit(1);
8863
+ }
8864
+ const { flags } = parseLiveFlags(args);
8865
+ const creds = loadCredentials();
8866
+ if (!creds || !creds.token) {
8867
+ console.error('Not logged in. Run: atris login');
8868
+ process.exit(1);
8869
+ }
8870
+ const body = { state: flags.state || 'running', scope: flags.scope };
8871
+ if (flags.note) body.working_on = flags.note;
8872
+ if (flags.scopeId) body.scope_id = flags.scopeId;
8873
+ const result = await apiRequestJson(`/members/${encodeURIComponent(name)}/heartbeat`, {
8874
+ method: 'POST',
8875
+ body: JSON.stringify(body),
8876
+ headers: { 'Content-Type': 'application/json' },
8877
+ token: creds.token,
8878
+ });
8879
+ if (!result.ok) {
8880
+ console.error(`Heartbeat failed: ${result.error || 'Unknown error'}`);
8881
+ process.exit(1);
8882
+ }
8883
+ if (flags.json) {
8884
+ console.log(JSON.stringify(result.data, null, 2));
8885
+ return;
8886
+ }
8887
+ const d = result.data || {};
8888
+ console.log(`${d.running ? '●' : '○'} ${d.member || name} → ${d.state}${d.working_on ? ` — ${d.working_on}` : ''} (visible on every surface)`);
8889
+ }
8890
+
8891
+ // atris member improvements — what the business computer improved, day by day.
8892
+ // GET /api/business/{id}/improvements/daily.
8893
+ async function memberImprovements(businessId, ...args) {
8894
+ if (!businessId) {
8895
+ console.error('Usage: atris member improvements <business-id> [--days N] [--json]');
8896
+ process.exit(1);
8897
+ }
8898
+ const { flags } = parseLiveFlags(args);
8899
+ const creds = loadCredentials();
8900
+ if (!creds || !creds.token) {
8901
+ console.error('Not logged in. Run: atris login');
8902
+ process.exit(1);
8903
+ }
8904
+ const days = flags.days && Number.isFinite(flags.days) ? flags.days : 30;
8905
+ const result = await apiRequestJson(`/business/${encodeURIComponent(businessId)}/improvements/daily?days=${days}`, { token: creds.token });
8906
+ if (!result.ok) {
8907
+ console.error(`Failed to fetch improvements: ${result.error || 'Unknown error'}`);
8908
+ process.exit(1);
8909
+ }
8910
+ if (flags.json) {
8911
+ console.log(JSON.stringify(result.data, null, 2));
8912
+ return;
8913
+ }
8914
+ const history = result.data?.history || [];
8915
+ console.log('');
8916
+ console.log(`daily improvements — business ${businessId}`);
8917
+ console.log('');
8918
+ if (history.length === 0) {
8919
+ console.log(' (no improvement history yet — first entry lands after the next nightly self-improve run)');
8920
+ }
8921
+ for (const day of history) {
8922
+ console.log(` ${day.date}: ${day.agents_improved}/${day.agents_total} agents improved`);
8923
+ for (const agent of day.agents || []) {
8924
+ const note = agent.note ? ` — ${agent.note}` : '';
8925
+ console.log(` ${agent.kept_count > 0 ? '+' : ' '} ${agent.name} (kept ${agent.kept_count}, score ${agent.best_score})${note}`);
8926
+ }
8927
+ }
8928
+ console.log('');
8929
+ }
8930
+
8110
8931
  // --- Command Dispatcher ---
8111
8932
 
8112
8933
  async function memberCommand(subcommand, ...args) {
@@ -8158,6 +8979,13 @@ async function memberCommand(subcommand, ...args) {
8158
8979
  return memberBlock(args[0], args[1], ...args.slice(2));
8159
8980
  case 'status':
8160
8981
  return memberStatus(args[0], ...args.slice(1));
8982
+ case 'live':
8983
+ return memberLive(...args);
8984
+ case 'heartbeat':
8985
+ case 'hb':
8986
+ return memberHeartbeat(args[0], ...args.slice(1));
8987
+ case 'improvements':
8988
+ return memberImprovements(args[0], ...args.slice(1));
8161
8989
  case 'history':
8162
8990
  return memberHistory(args[0], ...args.slice(1));
8163
8991
  case 'supervisor':
@@ -8186,11 +9014,14 @@ async function memberCommand(subcommand, ...args) {
8186
9014
  console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
8187
9015
  console.log(' wake <name> Read Mission state and decide tick/wait/ask/stop');
8188
9016
  console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
8189
- console.log(' loop <name> Repeat wake on a bounded cadence with a no-overlap lease');
9017
+ console.log(' loop <name|--all> Repeat wake on a bounded cadence with a no-overlap lease');
8190
9018
  console.log(' tick <name> Propose the next bounded experiment');
8191
9019
  console.log(' review <name> <id> Accept/discard an experiment with proof');
8192
9020
  console.log(' block <name> <id> Mark an experiment blocked with a human/orchestrator ask');
8193
- console.log(' status <name> Show goal, open experiment, value, ask, and recent log');
9021
+ console.log(' status <name|--all> Show goal, mission, activity, value, ask, and recent log');
9022
+ console.log(' live Show backend member projection (what iOS/web render) [--scope business --scope-id <id>]');
9023
+ console.log(' heartbeat <name> Mark a member running/done on every surface [--note "..."] [--done|--error]');
9024
+ console.log(' improvements <biz> Show daily self-improve history for a business [--days N]');
8194
9025
  console.log(' history <name> Show git history of member identity files (MEMBER.md, SOUL.md)');
8195
9026
  console.log(' supervisor recommendations Show advisory supervisor recommendations');
8196
9027
  console.log(' objective-generator proposals Show autonomous objective proposal');
@@ -8225,10 +9056,12 @@ async function memberCommand(subcommand, ...args) {
8225
9056
  console.log(' atris member loop growth --minutes 10 --interval 60 --json');
8226
9057
  console.log(' atris member alive growth --hourly --forever --execute --confirm-autonomy-policy --json');
8227
9058
  console.log(' atris member alive growth --install --hourly --forever --execute --confirm-autonomy-policy --json');
9059
+ console.log(' atris member alive --all --install --hourly --forever --dry-run');
8228
9060
  console.log(' atris member alive growth --minutes 480 --interval 900 --execute --confirm-autonomy-policy --json');
8229
9061
  console.log(' atris member loop growth --ticks 2 --interval 0 --json');
8230
9062
  console.log(' atris member tick growth --json');
8231
9063
  console.log(' atris member status growth');
9064
+ console.log(' atris member status --all');
8232
9065
  console.log(' atris member review growth exp_123 --accept --proof "validated" --value 4');
8233
9066
  console.log(' atris member archive old-member');
8234
9067
  console.log(' atris member purge-archived --days=60 --confirm "delete archived members"');
@@ -8236,4 +9069,4 @@ async function memberCommand(subcommand, ...args) {
8236
9069
  }
8237
9070
  }
8238
9071
 
8239
- module.exports = { memberCommand, findAllMembers, parseFrontmatter };
9072
+ module.exports = { memberCommand, findAllMembers, parseFrontmatter, wakeBootLines, buildMemberRunStartArgs };