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
@@ -17,6 +17,7 @@ const {
17
17
  summarizeReview,
18
18
  writeArtifact,
19
19
  } = require('../lib/endstate');
20
+ const daily = require('../lib/experiments/daily');
20
21
 
21
22
  const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
22
23
  const ROOT_FILES = ['README.md', 'validate.py', 'benchmark_validate.py', 'benchmark_runtime.py'];
@@ -520,8 +521,31 @@ function experimentsReplay(target = 'endstate') {
520
521
  experimentsCompare('endstate');
521
522
  }
522
523
 
524
+ function experimentsDaily(...args) {
525
+ ensureAtrisWorkspace();
526
+ const code = daily.runDaily(process.cwd(), args);
527
+ if (code) process.exit(code);
528
+ }
529
+
530
+ function experimentsQueue(subcommand, ...args) {
531
+ ensureAtrisWorkspace();
532
+ const root = process.cwd();
533
+ let code = 1;
534
+ if (subcommand === 'add') code = daily.queueAdd(root, args);
535
+ else if (subcommand === 'list') code = daily.queueList(root, args);
536
+ else {
537
+ console.error('Usage: atris experiments queue <add|list> ...');
538
+ code = 1;
539
+ }
540
+ if (code) process.exit(code);
541
+ }
542
+
523
543
  function experimentsCommand(subcommand, ...args) {
524
544
  switch (subcommand) {
545
+ case 'daily':
546
+ return experimentsDaily(...args);
547
+ case 'queue':
548
+ return experimentsQueue(args[0], ...args.slice(1));
525
549
  case 'init':
526
550
  case 'new':
527
551
  return experimentsInit(args[0]);
@@ -546,6 +570,8 @@ function experimentsCommand(subcommand, ...args) {
546
570
  console.log(' compare endstate Compare the latest baseline and stack receipts');
547
571
  console.log(' replay endstate Validate, dry-run, and compare the public benchmark flow');
548
572
  console.log(' benchmark [mode] Run validate/runtime/all benchmark harness');
573
+ console.log(' daily [--dry-run] [--force] [--json]');
574
+ console.log(' queue add|list Manage the daily experiment queue');
549
575
  console.log('');
550
576
  console.log('Examples:');
551
577
  console.log(' atris experiments init');
@@ -564,4 +590,6 @@ module.exports = {
564
590
  ensureExperimentsFramework,
565
591
  buildBenchmarkArtifact,
566
592
  parseRunOptions,
593
+ experimentsDaily,
594
+ experimentsQueue,
567
595
  };
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ // Feed: the business group feed from the terminal — read receipts, post receipts.
4
+ // Auth: existing atris login. Business: nearest .atris/business.json above cwd.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { ensureValidCredentials } = require('../utils/auth');
9
+ const { apiRequestJson } = require('../utils/api');
10
+
11
+ function showHelp() {
12
+ console.log('');
13
+ console.log('Usage: atris feed [list] [n] [--author <name>] [--since <7d|24h>] [--full] [--json]');
14
+ console.log(' atris feed post "<content>"');
15
+ console.log('');
16
+ console.log('Description:');
17
+ console.log(' Read and write the business group feed (the same feed as the web GM dashboard).');
18
+ console.log(' Posts should be receipts and state changes only: what shipped, what blocked, what changed.');
19
+ console.log('');
20
+ console.log('Options:');
21
+ console.log(' --author <name> Only posts whose author matches (name, email prefix, or id prefix).');
22
+ console.log(' --since <window> Only posts newer than e.g. 7d, 48h.');
23
+ console.log(' --full Print full post bodies instead of one line each.');
24
+ console.log(' --json Machine-readable output.');
25
+ console.log(' --help, -h Show this help.');
26
+ console.log('');
27
+ }
28
+
29
+ function findBusiness(startDir) {
30
+ let dir = startDir;
31
+ while (dir !== path.dirname(dir)) {
32
+ const file = path.join(dir, '.atris', 'business.json');
33
+ if (fs.existsSync(file)) {
34
+ try {
35
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
36
+ if (parsed.business_id) return { businessId: parsed.business_id, name: parsed.name || parsed.slug, root: dir };
37
+ } catch { /* fall through to parent dirs */ }
38
+ }
39
+ dir = path.dirname(dir);
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function loadAuthorAliases(workspaceRoot) {
45
+ const file = path.join(workspaceRoot, '.atris', 'feed_authors.json');
46
+ try {
47
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ function parseSince(raw) {
54
+ const match = /^(\d+)([dh])$/.exec(String(raw || '').trim());
55
+ if (!match) return null;
56
+ const amount = parseInt(match[1], 10);
57
+ const ms = match[2] === 'd' ? amount * 24 * 3600 * 1000 : amount * 3600 * 1000;
58
+ return Date.now() - ms;
59
+ }
60
+
61
+ function authorLabel(post, aliases, selfId, selfEmail) {
62
+ if (post.post_type === 'agent_post') return post.agent_name || post.author_display_name || 'agent';
63
+ if (post.author_display_name) return post.author_display_name;
64
+ const id = post.author_id || '';
65
+ if (aliases[id]) return aliases[id];
66
+ if (selfId && id === selfId) return (selfEmail || 'me').split('@')[0];
67
+ return id ? id.slice(0, 8) : '?';
68
+ }
69
+
70
+ async function fetchPosts(businessId, token, want) {
71
+ const pageSize = 100;
72
+ let all = [];
73
+ for (let offset = 0; all.length < want; offset += pageSize) {
74
+ const res = await apiRequestJson(
75
+ `/business/${businessId}/feed?limit=${pageSize}&offset=${offset}`,
76
+ { token, timeoutMs: 20000 }
77
+ );
78
+ const statusCode = res.statusCode || res.status;
79
+ if (statusCode && statusCode >= 400) {
80
+ throw new Error(`Feed fetch failed (${statusCode}): ${JSON.stringify(res.data)}`);
81
+ }
82
+ const page = Array.isArray(res.data?.posts) ? res.data.posts : Array.isArray(res.data) ? res.data : [];
83
+ all = all.concat(page);
84
+ if (page.length < pageSize) break;
85
+ }
86
+ return all;
87
+ }
88
+
89
+ async function feedCommand(args = []) {
90
+ if (args.includes('--help') || args.includes('-h')) {
91
+ showHelp();
92
+ return 0;
93
+ }
94
+
95
+ const business = findBusiness(process.cwd());
96
+ if (!business) {
97
+ console.error('✗ No .atris/business.json found above this directory. Run from a bound workspace.');
98
+ return 1;
99
+ }
100
+
101
+ const auth = await ensureValidCredentials(apiRequestJson);
102
+ if (auth.error || !auth.credentials?.token) {
103
+ console.error(`✗ Not logged in (${auth.error || 'no token'}). Run: atris login`);
104
+ return 1;
105
+ }
106
+ const token = auth.credentials.token;
107
+ const selfId = auth.credentials.user_id || auth.user?.id || null;
108
+ const selfEmail = auth.credentials.email || auth.user?.email || null;
109
+
110
+ const positional = args.filter((a) => !a.startsWith('--'));
111
+ const sub = positional[0] || 'list';
112
+
113
+ if (sub === 'post') {
114
+ const content = positional.slice(1).join(' ').trim();
115
+ if (!content) {
116
+ console.error('✗ Nothing to post. Usage: atris feed post "<content>"');
117
+ return 1;
118
+ }
119
+ const res = await apiRequestJson(`/business/${business.businessId}/feed`, {
120
+ method: 'POST',
121
+ token,
122
+ body: { content: content.slice(0, 10000) },
123
+ timeoutMs: 20000,
124
+ });
125
+ if (!res.data?.id) {
126
+ console.error(`✗ Post failed: ${JSON.stringify(res.data)}`);
127
+ return 1;
128
+ }
129
+ console.log(`✓ Posted to ${business.name || 'business'} feed (${res.data.id})`);
130
+ return 0;
131
+ }
132
+
133
+ if (sub !== 'list') {
134
+ console.error(`✗ Unknown subcommand: ${sub}`);
135
+ showHelp();
136
+ return 1;
137
+ }
138
+
139
+ const countArg = parseInt(positional[1], 10);
140
+ const count = Number.isFinite(countArg) ? Math.max(1, Math.min(300, countArg)) : 15;
141
+ const authorFlag = args.includes('--author') ? String(args[args.indexOf('--author') + 1] || '').toLowerCase() : null;
142
+ const sinceFlag = args.includes('--since') ? parseSince(args[args.indexOf('--since') + 1]) : null;
143
+ if (args.includes('--since') && sinceFlag === null) {
144
+ console.error('✗ Bad --since window. Use forms like 7d or 48h.');
145
+ return 1;
146
+ }
147
+ const full = args.includes('--full');
148
+ const json = args.includes('--json');
149
+
150
+ const aliases = loadAuthorAliases(business.root);
151
+ // Over-fetch when filtering so the filtered result can still reach `count`.
152
+ const fetchWant = authorFlag || sinceFlag ? 300 : count;
153
+ let posts = await fetchPosts(business.businessId, token, fetchWant);
154
+
155
+ if (sinceFlag) posts = posts.filter((p) => Date.parse(p.created_at) >= sinceFlag);
156
+ if (authorFlag) {
157
+ posts = posts.filter((p) => {
158
+ const label = authorLabel(p, aliases, selfId, selfEmail).toLowerCase();
159
+ return label.includes(authorFlag) || String(p.author_id || '').startsWith(authorFlag);
160
+ });
161
+ }
162
+ posts = posts.slice(0, count);
163
+
164
+ if (json) {
165
+ console.log(JSON.stringify(posts.map((p) => ({
166
+ id: p.id,
167
+ author: authorLabel(p, aliases, selfId, selfEmail),
168
+ type: p.post_type,
169
+ created_at: p.created_at,
170
+ content: p.content,
171
+ })), null, 2));
172
+ return 0;
173
+ }
174
+
175
+ if (!posts.length) {
176
+ console.log('No posts matched.');
177
+ return 0;
178
+ }
179
+
180
+ for (const p of posts) {
181
+ const when = String(p.created_at || '').slice(0, 16).replace('T', ' ');
182
+ const who = authorLabel(p, aliases, selfId, selfEmail);
183
+ if (full) {
184
+ console.log(`─── ${who} — ${when}`);
185
+ console.log(String(p.content || '').trim());
186
+ console.log('');
187
+ } else {
188
+ const oneLine = String(p.content || '').replace(/\s+/g, ' ').slice(0, 120);
189
+ console.log(`[${when}] ${who}: ${oneLine}`);
190
+ }
191
+ }
192
+ return 0;
193
+ }
194
+
195
+ module.exports = {
196
+ feedCommand,
197
+ findBusiness,
198
+ loadAuthorAliases,
199
+ parseSince,
200
+ authorLabel,
201
+ fetchPosts,
202
+ };
@@ -39,10 +39,15 @@ function getAuth() {
39
39
  return { token: creds.token, email: creds.email || 'unknown' };
40
40
  }
41
41
 
42
+ function rejectMissingFeedbackMessage() {
43
+ console.error('Error: feedback message is required.');
44
+ printHelp(console.error);
45
+ process.exit(1);
46
+ }
47
+
42
48
  async function submitFeedback(message, opts = {}) {
43
- if (!message) {
44
- console.error('Usage: atris feedback "your message here" [--business <slug|id>]');
45
- process.exit(1);
49
+ if (!message || !String(message).trim()) {
50
+ rejectMissingFeedbackMessage();
46
51
  }
47
52
 
48
53
  const { token } = getAuth();
@@ -65,15 +70,20 @@ async function submitFeedback(message, opts = {}) {
65
70
  body,
66
71
  });
67
72
 
68
- if (!result.ok) {
69
- console.error(`Error: ${result.error || 'Failed to submit feedback'}`);
73
+ if (!result.ok || !result.data?.feedback_id) {
74
+ const parts = [];
75
+ if (result.error) parts.push(result.error);
76
+ if (result.data && typeof result.data === 'object') {
77
+ const serverMsg = result.data.detail || result.data.error || result.data.message;
78
+ if (serverMsg && serverMsg !== result.error) parts.push(String(serverMsg));
79
+ }
80
+ if (result.text && !result.data) parts.push(result.text.trim());
81
+ if (!parts.length) parts.push('Failed to submit feedback (no feedback_id returned)');
82
+ console.error(`Error: ${parts.join(' — ')}`);
70
83
  process.exit(1);
71
84
  }
72
85
 
73
- console.log('Feedback submitted.');
74
- if (result.data?.feedback_id) {
75
- console.log(` ID: ${result.data.feedback_id}`);
76
- }
86
+ console.log(`Feedback submitted. ID: ${result.data.feedback_id}`);
77
87
  }
78
88
 
79
89
  async function fetchFeedbackItems({ token, businessId, limit = 100, status = null } = {}) {
@@ -279,7 +289,7 @@ function directFeedbackMessage(args) {
279
289
  }
280
290
 
281
291
  function rejectUnknownFeedbackCommand(subcommand) {
282
- console.error(`Unknown feedback command: ${subcommand || '(empty)'}`);
292
+ fs.writeSync(2, `Unknown feedback command: ${subcommand || '(empty)'}\n`);
283
293
  printHelp(console.error);
284
294
  process.exit(1);
285
295
  }
@@ -293,9 +303,14 @@ async function feedbackCommand() {
293
303
  process.exit(1);
294
304
  }
295
305
 
306
+ if (args.length === 0) {
307
+ await listFeedback();
308
+ return;
309
+ }
310
+
296
311
  const subcommand = args[0];
297
312
 
298
- if (!subcommand || subcommand === 'list') {
313
+ if (subcommand === 'list') {
299
314
  await listFeedback();
300
315
  return;
301
316
  }
@@ -306,7 +321,10 @@ async function feedbackCommand() {
306
321
  }
307
322
 
308
323
  if (subcommand === 'submit') {
309
- const message = args.slice(1).join(' ');
324
+ const message = args.slice(1).join(' ').trim();
325
+ if (!message) {
326
+ rejectMissingFeedbackMessage();
327
+ }
310
328
  await submitFeedback(message, { businessId });
311
329
  return;
312
330
  }
@@ -328,6 +346,10 @@ async function feedbackCommand() {
328
346
  return;
329
347
  }
330
348
 
349
+ if (args.length === 1 && !String(args[0]).trim()) {
350
+ rejectMissingFeedbackMessage();
351
+ }
352
+
331
353
  const message = directFeedbackMessage(args);
332
354
  if (message) {
333
355
  await submitFeedback(message, { businessId });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * atris fleet-report [business] [--all-alive] [--wake] [--dry-run]
3
+ *
4
+ * Deliver the daily report to business AI computers so every computer
5
+ * that is alive can see it at /workspace/atris/reports/daily-YYYY-MM-DD.md.
6
+ *
7
+ * Why push from the fleet side instead of a cron on the box: computers
8
+ * hold no API token on disk (Iron Dome P4 - /workspace persists, secrets
9
+ * never touch disk), so the fleet layer fetches the report with the
10
+ * owner token and writes it over the existing /terminal primitive.
11
+ *
12
+ * atris fleet-report agentgrads --wake # one computer, wake if asleep
13
+ * atris fleet-report --all-alive # every running computer (cron mode)
14
+ */
15
+
16
+ const { loadCredentials } = require('../utils/auth');
17
+ const { apiRequestJson } = require('../utils/api');
18
+
19
+ function sleep(ms) {
20
+ return new Promise((r) => setTimeout(r, ms));
21
+ }
22
+
23
+ async function listBusinesses(token) {
24
+ const res = await apiRequestJson('/business/', { method: 'GET', token });
25
+ if (!res.ok) throw new Error(`Could not list businesses: ${res.status || res.error}`);
26
+ return res.data || [];
27
+ }
28
+
29
+ async function computerStatus(token, businessId) {
30
+ const res = await apiRequestJson(`/business/${businessId}/ai-computer/status`, {
31
+ method: 'GET',
32
+ token,
33
+ });
34
+ return res.ok ? res.data : null;
35
+ }
36
+
37
+ async function wakeComputer(token, businessId, maxWaitSec = 90) {
38
+ await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token });
39
+ const start = Date.now();
40
+ while (Date.now() - start < maxWaitSec * 1000) {
41
+ await sleep(3000);
42
+ const status = await computerStatus(token, businessId);
43
+ if (status && status.status === 'running' && status.endpoint) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ async function fetchDailyReport(token, businessId) {
49
+ const res = await apiRequestJson(`/business/${businessId}/daily-report`, {
50
+ method: 'GET',
51
+ token,
52
+ timeoutMs: 60000,
53
+ });
54
+ if (!res.ok) throw new Error(`daily-report fetch failed: ${res.status || res.error}`);
55
+ return res.data;
56
+ }
57
+
58
+ async function writeReportToComputer(token, businessId, workspaceId, report) {
59
+ const date = report.date || new Date().toISOString().slice(0, 10);
60
+ const remotePath = `/workspace/atris/reports/daily-${date}.md`;
61
+ const b64 = Buffer.from(report.markdown || '', 'utf8').toString('base64');
62
+ const command =
63
+ `mkdir -p /workspace/atris/reports && ` +
64
+ `echo '${b64}' | base64 -d > ${remotePath} && ` +
65
+ `ln -sf ${remotePath} /workspace/atris/reports/latest.md && ` +
66
+ `echo WROTE:${remotePath}`;
67
+ if (command.length > 10000) {
68
+ throw new Error(`report too large for one terminal write (${command.length} chars)`);
69
+ }
70
+ const res = await apiRequestJson(`/business/${businessId}/workspaces/${workspaceId}/terminal`, {
71
+ method: 'POST',
72
+ token,
73
+ body: { command, timeout: 30 },
74
+ timeoutMs: 45000,
75
+ });
76
+ if (!res.ok) throw new Error(`terminal write failed: ${res.status || res.error}`);
77
+ const out = (res.data && (res.data.stdout || res.data.output)) || '';
78
+ if (!out.includes('WROTE:')) throw new Error(`terminal write not confirmed: ${out.slice(0, 200)}`);
79
+ return remotePath;
80
+ }
81
+
82
+ async function deliverToBusiness(token, biz, { wake, dryRun }) {
83
+ const label = biz.slug || biz.name || biz.id;
84
+ const status = await computerStatus(token, biz.id);
85
+ const running = status && status.status === 'running' && status.endpoint;
86
+
87
+ if (!running && !wake) {
88
+ console.log(` ${label}: computer not alive, skipping`);
89
+ return { business: label, delivered: false, reason: 'asleep' };
90
+ }
91
+ if (!running && wake) {
92
+ process.stdout.write(` ${label}: waking... `);
93
+ const ok = await wakeComputer(token, biz.id);
94
+ console.log(ok ? 'awake' : 'wake timeout');
95
+ if (!ok) return { business: label, delivered: false, reason: 'wake-timeout' };
96
+ }
97
+
98
+ const report = await fetchDailyReport(token, biz.id);
99
+ const board = report.scoreboard || null;
100
+ const pnl = board
101
+ ? ` | mrr $${board.revenue_mrr_usd} cost $${(board.compute_cost_usd + (board.ec2_cost_usd || 0)).toFixed(2)}/d profit $${board.profit_daily_usd}/d`
102
+ : '';
103
+ if (dryRun) {
104
+ console.log(` ${label}: dry-run, report ${String(report.markdown || '').length} chars${pnl}`);
105
+ return { business: label, delivered: false, reason: 'dry-run', scoreboard: board };
106
+ }
107
+ const remotePath = await writeReportToComputer(token, biz.id, biz.workspace_id, report);
108
+ console.log(` ${label}: delivered ${remotePath}${pnl}`);
109
+ return { business: label, delivered: true, path: remotePath, scoreboard: board };
110
+ }
111
+
112
+ async function leaderboard(token, businesses) {
113
+ const rows = [];
114
+ const pool = [...businesses];
115
+ const workers = Array.from({ length: 5 }, async () => {
116
+ let biz;
117
+ while ((biz = pool.shift())) {
118
+ try {
119
+ const report = await fetchDailyReport(token, biz.id);
120
+ const b = report.scoreboard;
121
+ if (b) rows.push({ slug: biz.slug || biz.name, ...b });
122
+ } catch {
123
+ /* business without a readable report stays off the board */
124
+ }
125
+ }
126
+ });
127
+ await Promise.all(workers);
128
+
129
+ rows.sort(
130
+ (a, b) =>
131
+ (b.revenue_collected_30d_usd || 0) - (a.revenue_collected_30d_usd || 0) ||
132
+ (b.profit_daily_usd || 0) - (a.profit_daily_usd || 0) ||
133
+ (b.revenue_mrr_usd || 0) - (a.revenue_mrr_usd || 0)
134
+ );
135
+
136
+ console.log('Computer leaderboard - collected 30d, then profit/day');
137
+ const fmt = (n) => `$${(n || 0).toFixed(2)}`;
138
+ rows.forEach((r, i) => {
139
+ console.log(
140
+ ` ${String(i + 1).padStart(2)}. ${r.slug.padEnd(22)} collected ${fmt(
141
+ r.revenue_collected_30d_usd
142
+ ).padStart(10)} mrr ${fmt(r.revenue_mrr_usd).padStart(10)} profit/d ${fmt(
143
+ r.profit_daily_usd
144
+ ).padStart(9)}`
145
+ );
146
+ });
147
+ if (rows.length === 0) console.log(' (no scoreboards yet)');
148
+ }
149
+
150
+ async function fleetReport() {
151
+ const args = process.argv.slice(3);
152
+ const allAlive = args.includes('--all-alive');
153
+ const wake = args.includes('--wake');
154
+ const dryRun = args.includes('--dry-run');
155
+ const board = args.includes('--leaderboard');
156
+ const slug = args.find((a) => !a.startsWith('--'));
157
+
158
+ if (!allAlive && !slug && !board) {
159
+ console.log('Usage: atris fleet-report <business> [--wake] | --all-alive [--dry-run] | --leaderboard');
160
+ process.exit(1);
161
+ }
162
+
163
+ const creds = loadCredentials();
164
+ if (!creds || !creds.token) {
165
+ console.error('Not logged in. Run: atris login');
166
+ process.exit(1);
167
+ }
168
+ const token = creds.token;
169
+
170
+ const businesses = await listBusinesses(token);
171
+ if (board) {
172
+ await leaderboard(token, businesses);
173
+ return;
174
+ }
175
+ const targets = allAlive
176
+ ? businesses
177
+ : businesses.filter(
178
+ (b) => b.slug === slug || (b.name || '').toLowerCase() === slug.toLowerCase()
179
+ );
180
+ if (targets.length === 0) {
181
+ console.error(`No business matching "${slug}"`);
182
+ process.exit(1);
183
+ }
184
+
185
+ console.log(`Fleet daily report - ${targets.length} target(s)`);
186
+ const results = [];
187
+ for (const biz of targets) {
188
+ try {
189
+ results.push(await deliverToBusiness(token, biz, { wake, dryRun }));
190
+ } catch (err) {
191
+ console.log(` ${biz.slug || biz.name}: FAILED - ${err.message}`);
192
+ results.push({ business: biz.slug || biz.name, delivered: false, reason: err.message });
193
+ }
194
+ }
195
+ const delivered = results.filter((r) => r.delivered).length;
196
+ const boards = results.map((r) => r.scoreboard).filter(Boolean);
197
+ if (boards.length > 0) {
198
+ const mrr = boards.reduce((s, b) => s + (b.revenue_mrr_usd || 0), 0);
199
+ const profit = boards.reduce((s, b) => s + (b.profit_daily_usd || 0), 0);
200
+ console.log(`Fleet: $${mrr} MRR, $${profit.toFixed(2)}/day profit across ${boards.length} scoreboards`);
201
+ }
202
+ console.log(`Done: ${delivered}/${targets.length} delivered`);
203
+ process.exitCode = delivered > 0 || dryRun || results.every((r) => r.reason === 'asleep') ? 0 : 1;
204
+ }
205
+
206
+ module.exports = { fleetReport };
@@ -0,0 +1,38 @@
1
+ const { createOfficialCliCommand } = require('../lib/official-cli-integration');
2
+
3
+ const githubCommand = createOfficialCliCommand({
4
+ name: 'github',
5
+ binary: 'gh',
6
+ versionArgs: ['--version'],
7
+ authArgs: ['auth', 'status'],
8
+ installHint: 'https://cli.github.com/',
9
+ loginHint: 'gh auth login',
10
+ commands: [
11
+ {
12
+ usage: 'pr list',
13
+ match: ['pr', 'list'],
14
+ forward: ['pr', 'list'],
15
+ description: 'list pull requests',
16
+ },
17
+ {
18
+ usage: 'pr create',
19
+ match: ['pr', 'create'],
20
+ forward: ['pr', 'create'],
21
+ description: 'create a pull request',
22
+ },
23
+ {
24
+ usage: 'pr checks',
25
+ match: ['pr', 'checks'],
26
+ forward: ['pr', 'checks'],
27
+ description: 'show pull request checks',
28
+ },
29
+ {
30
+ usage: 'pr view',
31
+ match: ['pr', 'view'],
32
+ forward: ['pr', 'view'],
33
+ description: 'show pull request details',
34
+ },
35
+ ],
36
+ });
37
+
38
+ module.exports = { githubCommand };