atris 3.35.0 → 3.37.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 (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +576 -267
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3144 -338
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +96 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
@@ -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 {
@@ -614,8 +674,10 @@ function resolveMemberRunMissionId(name, args = []) {
614
674
  const runtimeId = memberRunRunnableMissionId(purpose.runtimeMission?.id, missionMap);
615
675
  if (runtimeId) return runtimeId;
616
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
617
679
  const goals = loadMemberGoals(name, paths);
618
- const goal = activeGoal(goals);
680
+ const goal = (goals.goals || []).find((candidate) => candidate.status === 'active') || null;
619
681
  const goalId = memberRunRunnableMissionId(goal?.mission_id, missionMap);
620
682
  if (goalId) return goalId;
621
683
 
@@ -646,6 +708,7 @@ const MEMBER_RUN_START_VALUE_FLAGS = [
646
708
 
647
709
  const MEMBER_RUN_START_BOOLEAN_FLAGS = [
648
710
  '--json',
711
+ '--no-verify',
649
712
  '--worktree',
650
713
  '--shared-checkout',
651
714
  '--no-worktree',
@@ -681,16 +744,34 @@ function memberRunTruthRule(args = []) {
681
744
  || 'say what changed, what was checked, and what is still unproven';
682
745
  }
683
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
+
684
763
  function memberRunUsefulTarget() {
685
764
  try {
686
765
  const { nextMoves: pickNextMoves, isGenericInboxPlaceholder } = require('../lib/next-moves');
687
766
  const moves = pickNextMoves(process.cwd(), 5);
767
+ const waitingWishes = operatorWaitingWishTexts();
688
768
  const candidate = moves.find((move) => {
689
769
  const title = cleanMemberRunPhrase(move?.title);
690
770
  if (!title) return false;
691
771
  if (isGenericInboxPlaceholder(title)) return false;
692
772
  if (/^mission xp\s*:/i.test(title)) return false;
693
773
  if (/^decide and start the next useful mission after:/i.test(title)) return false;
774
+ if (waitingWishes.has(title.toLowerCase())) return false;
694
775
  return true;
695
776
  });
696
777
  return candidate?.title ? cleanMemberRunPhrase(candidate.title) : '';
@@ -733,17 +814,18 @@ function pushFlagWhenPresent(out, args, name) {
733
814
  if (hasFlag(args, name)) out.push(name);
734
815
  }
735
816
 
736
- function startMemberRunMission(name, missionText, args = []) {
737
- const paths = requireMemberDir(name);
738
- const owner = paths.storageName || name;
817
+ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cwd()) {
739
818
  const startArgs = [
740
819
  'mission',
741
820
  'start',
742
821
  missionText,
743
822
  '--owner',
744
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.
745
827
  '--runner',
746
- readFlag(args, '--runner', 'codex_goal'),
828
+ readFlag(args, '--runner', defaultObjectiveRunner(args)),
747
829
  '--lane',
748
830
  readFlag(args, '--lane', 'workspace'),
749
831
  ];
@@ -755,14 +837,43 @@ function startMemberRunMission(name, missionText, args = []) {
755
837
  pushFlagValue(startArgs, args, '--minutes');
756
838
  pushFlagValue(startArgs, args, '--hours');
757
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
+ }
758
847
  pushFlagWhenPresent(startArgs, args, '--always-on');
759
848
  pushFlagWhenPresent(startArgs, args, '--xp-task');
760
849
  pushFlagWhenPresent(startArgs, args, '--agent-xp');
761
850
  pushFlagWhenPresent(startArgs, args, '--spend-full-budget');
762
851
  pushFlagWhenPresent(startArgs, args, '--use-whole-budget');
763
852
  pushFlagWhenPresent(startArgs, args, '--stop-when-done');
764
- 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
+ }
765
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);
766
877
 
767
878
  const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
768
879
  try {
@@ -1603,14 +1714,43 @@ function readRecentWakeReceiptEvidence(name) {
1603
1714
  }
1604
1715
 
1605
1716
  function readJsonlRowsIfExists(filePath, maxRows = 80) {
1717
+ const tailBytes = 256 * 1024;
1718
+ let fd;
1606
1719
  let text = '';
1720
+ let truncated = false;
1607
1721
  try {
1608
- 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;
1609
1741
  } catch {
1610
1742
  return [];
1743
+ } finally {
1744
+ if (fd !== undefined) {
1745
+ try { fs.closeSync(fd); } catch {}
1746
+ }
1611
1747
  }
1612
1748
  const rows = [];
1613
- 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));
1614
1754
  for (const line of lines) {
1615
1755
  try {
1616
1756
  rows.push(JSON.parse(line));
@@ -2870,6 +3010,17 @@ function failureCoveredByPassLesson(line, passLessonText = '') {
2870
3010
  return /spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(passLessonText)
2871
3011
  && (!target || passLessonText.includes(target) || (targetTail && passLessonText.includes(targetTail)));
2872
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
+ }
2873
3024
  return false;
2874
3025
  }
2875
3026
 
@@ -3762,6 +3913,7 @@ function findAllMembers(teamDir) {
3762
3913
  const content = fs.readFileSync(fullPath, 'utf8');
3763
3914
  const fm = parseFrontmatter(content);
3764
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
3765
3917
 
3766
3918
  const name = entry.replace('.md', '');
3767
3919
  members.push({
@@ -7569,6 +7721,127 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
7569
7721
  };
7570
7722
  }
7571
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
+
7572
7845
  async function memberWake(name, ...args) {
7573
7846
  const asJson = hasFlag(args, '--json');
7574
7847
  const execute = hasFlag(args, '--execute') && !hasFlag(args, '--dry-run');
@@ -7579,20 +7852,25 @@ async function memberWake(name, ...args) {
7579
7852
  file: readFlag(args, '--domain-file', ''),
7580
7853
  name: readFlag(args, '--domain-name', ''),
7581
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
+ }
7582
7872
  const result = await runMemberWake(name, { execute, confirmed, force, domainInput });
7583
- printJsonOrText(
7584
- result,
7585
- [
7586
- `Wake: ${name}`,
7587
- `Decision: ${result.decision}`,
7588
- `Reason: ${result.reason}`,
7589
- `Mode: ${result.mode}${result.executed ? ' executed' : ''}`,
7590
- ...(result.ask ? [`Ask: ${result.ask}`] : []),
7591
- `Next: ${result.next_command}`,
7592
- `Receipt: ${path.relative(process.cwd(), result.receipt_path)}`,
7593
- ],
7594
- asJson,
7595
- );
7873
+ printJsonOrText(result, wakeBootLines(name, result), asJson);
7596
7874
  }
7597
7875
 
7598
7876
  function memberAlive(name, ...args) {
@@ -7874,6 +8152,13 @@ async function memberLoop(name, ...args) {
7874
8152
  let earlyExit = null;
7875
8153
  let consecutiveIdle = 0;
7876
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;
7877
8162
 
7878
8163
  try {
7879
8164
  for (let index = 0; index < ticks; index += 1) {
@@ -7945,6 +8230,29 @@ async function memberLoop(name, ...args) {
7945
8230
  };
7946
8231
  tickResults.push(tick);
7947
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
+ }
7948
8256
  }
7949
8257
  } catch (error) {
7950
8258
  failed = true;
@@ -7959,6 +8267,7 @@ async function memberLoop(name, ...args) {
7959
8267
  fs.appendFileSync(tickLogPath, JSON.stringify(tick) + '\n', 'utf8');
7960
8268
  break;
7961
8269
  }
8270
+ if (earlyExit) break;
7962
8271
  if (execute) {
7963
8272
  const last = tickResults[tickResults.length - 1];
7964
8273
  if (last && last.productive) {
@@ -8007,6 +8316,7 @@ async function memberLoop(name, ...args) {
8007
8316
  duration_ms_actual: Date.parse(finishedAt) - Date.parse(startedAt),
8008
8317
  decisions,
8009
8318
  early_exit: earlyExit,
8319
+ stop: earlyExit?.stop || null,
8010
8320
  blocked_on_human: earlyExit?.blocked_on_human === true,
8011
8321
  needs_user: earlyExit?.needs_user === true,
8012
8322
  next_command: earlyExit?.next_command || null,
@@ -8328,6 +8638,16 @@ function memberStatus(name, ...args) {
8328
8638
  const mission = memberMissionSummary(owner, memberRunMissionMap());
8329
8639
  const activity = memberLastActivity(owner, paths);
8330
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';
8331
8651
  const payload = {
8332
8652
  ok: true,
8333
8653
  action: 'status',
@@ -8339,6 +8659,10 @@ function memberStatus(name, ...args) {
8339
8659
  current_experiment: goalPlane.current || null,
8340
8660
  last_reviewed: goalPlane.lastReviewed || null,
8341
8661
  value: goalPlane.value,
8662
+ switch: {
8663
+ awake: memberSwitch.awake !== false,
8664
+ loops: memberSwitch.loops || {},
8665
+ },
8342
8666
  mission: mission.latest ? {
8343
8667
  id: mission.latest.id || null,
8344
8668
  name: mission.name,
@@ -8354,20 +8678,26 @@ function memberStatus(name, ...args) {
8354
8678
  goals_path: paths.goalsJson,
8355
8679
  goals_md_path: paths.goalsMd,
8356
8680
  };
8681
+ const s = wakeStyle();
8682
+ const role = wakeMemberRole(name);
8683
+ const ballWithHuman = goalPlane.needsUser === true || goalPlane.current?.status === 'proposed';
8357
8684
  printJsonOrText(
8358
8685
  payload,
8359
8686
  [
8360
- `Member: ${name}`,
8361
- `State: ${goalPlane.stateLabel}`,
8362
- `Goal: ${goalPlane.goal?.title || 'No goal yet'}`,
8363
- `Current: ${goalPlane.current ? `${goalPlane.current.status} - ${goalPlane.current.title}` : 'No open experiment'}`,
8364
- ...(goalPlane.ask ? [`Ask: ${goalPlane.ask}`] : []),
8365
- `Value: ${goalPlane.value.line}`,
8366
- `Mission: ${mission.latest ? `${mission.name} (${mission.state}, last tick ${mission.last_tick_age})` : 'No owned mission'}`,
8367
- `Activity: ${activity.age}`,
8368
- `Verdict: ${verdict}`,
8369
- `Next: ${goalPlane.nextCommand}`,
8370
- ...(goalPlane.logs.length ? ['Recent log:', ...goalPlane.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
+ '',
8371
8701
  ],
8372
8702
  asJson,
8373
8703
  );
@@ -8461,6 +8791,143 @@ function memberHistory(name, ...args) {
8461
8791
  }
8462
8792
  }
8463
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
+
8464
8931
  // --- Command Dispatcher ---
8465
8932
 
8466
8933
  async function memberCommand(subcommand, ...args) {
@@ -8512,6 +8979,13 @@ async function memberCommand(subcommand, ...args) {
8512
8979
  return memberBlock(args[0], args[1], ...args.slice(2));
8513
8980
  case 'status':
8514
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));
8515
8989
  case 'history':
8516
8990
  return memberHistory(args[0], ...args.slice(1));
8517
8991
  case 'supervisor':
@@ -8545,6 +9019,9 @@ async function memberCommand(subcommand, ...args) {
8545
9019
  console.log(' review <name> <id> Accept/discard an experiment with proof');
8546
9020
  console.log(' block <name> <id> Mark an experiment blocked with a human/orchestrator ask');
8547
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]');
8548
9025
  console.log(' history <name> Show git history of member identity files (MEMBER.md, SOUL.md)');
8549
9026
  console.log(' supervisor recommendations Show advisory supervisor recommendations');
8550
9027
  console.log(' objective-generator proposals Show autonomous objective proposal');
@@ -8592,4 +9069,4 @@ async function memberCommand(subcommand, ...args) {
8592
9069
  }
8593
9070
  }
8594
9071
 
8595
- module.exports = { memberCommand, findAllMembers, parseFrontmatter };
9072
+ module.exports = { memberCommand, findAllMembers, parseFrontmatter, wakeBootLines, buildMemberRunStartArgs };