atris 3.35.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 (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 +565 -265
  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 +3029 -339
  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 +79 -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
@@ -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
  };
@@ -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 };
package/commands/gm.js CHANGED
@@ -629,12 +629,35 @@ function render(state) {
629
629
  for (const command of state.next_commands) console.log(`- ${command}`);
630
630
  }
631
631
 
632
+ // `gm <name>` is how people greet a team member in chat ("gm maze"). When the
633
+ // positional names a real member (and not the manager persona), the same phrase
634
+ // works in the terminal: wake that member instead of entering manager mode.
635
+ function wakeableMember(args) {
636
+ const name = slugify(positional(args)[0]);
637
+ if (!name || name === 'game-manager' || flag(args, '--manager')) return null;
638
+ const memberDir = path.join(process.cwd(), 'atris', 'team', name);
639
+ if (!fs.existsSync(path.join(memberDir, 'MEMBER.md'))) return null;
640
+ return name;
641
+ }
642
+
632
643
  async function gmCommand(...args) {
633
644
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
634
645
  showHelp();
635
646
  return;
636
647
  }
637
648
 
649
+ const member = wakeableMember(args);
650
+ if (member) {
651
+ const { isMemberAwake } = require('../lib/member-switches');
652
+ if (!isMemberAwake(member)) {
653
+ console.log(`${member} is asleep`);
654
+ return;
655
+ }
656
+ const { memberCommand } = require('./member');
657
+ const passthrough = args.filter(arg => arg !== member);
658
+ return memberCommand('wake', member, ...passthrough);
659
+ }
660
+
638
661
  const state = gmState(args);
639
662
  if (hasFlag(args, '--json')) {
640
663
  console.log(JSON.stringify(state, null, 2));
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const SCOREBOARD_FILE = path.join('.atris', 'state', 'scoreboard.json');
7
+ const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
8
+
9
+ function normalizeText(value) {
10
+ return String(value || '')
11
+ .replace(/[\u2013\u2014]/g, '-')
12
+ .replace(/\s+/g, ' ')
13
+ .trim()
14
+ .toLowerCase();
15
+ }
16
+
17
+ function scoreboardPath(root = process.cwd()) {
18
+ return path.join(root, SCOREBOARD_FILE);
19
+ }
20
+
21
+ function emptyScoreboard() {
22
+ return { goal: null, metrics: [], updated_at: null };
23
+ }
24
+
25
+ function readScoreboard(root = process.cwd()) {
26
+ const file = scoreboardPath(root);
27
+ if (!fs.existsSync(file)) return emptyScoreboard();
28
+ try {
29
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
30
+ return {
31
+ goal: normalizeText(parsed?.goal) || null,
32
+ metrics: Array.isArray(parsed?.metrics)
33
+ ? parsed.metrics
34
+ .map((metric) => ({
35
+ name: normalizeText(metric?.name),
36
+ value: normalizeText(metric?.value),
37
+ }))
38
+ .filter((metric) => metric.name && metric.value)
39
+ : [],
40
+ updated_at: parsed?.updated_at || null,
41
+ };
42
+ } catch {
43
+ return emptyScoreboard();
44
+ }
45
+ }
46
+
47
+ function writeScoreboard(scoreboard, root = process.cwd()) {
48
+ const file = scoreboardPath(root);
49
+ fs.mkdirSync(path.dirname(file), { recursive: true });
50
+ const next = {
51
+ goal: normalizeText(scoreboard.goal),
52
+ metrics: Array.isArray(scoreboard.metrics) ? scoreboard.metrics : [],
53
+ updated_at: new Date().toISOString(),
54
+ };
55
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
56
+ fs.writeFileSync(temp, `${JSON.stringify(next)}\n`, 'utf8');
57
+ fs.renameSync(temp, file);
58
+ return next;
59
+ }
60
+
61
+ function timestampMs(value) {
62
+ if (value === null || value === undefined || value === '') return null;
63
+ const numeric = Number(value);
64
+ if (Number.isFinite(numeric)) return numeric;
65
+ const parsed = new Date(value).getTime();
66
+ return Number.isFinite(parsed) ? parsed : null;
67
+ }
68
+
69
+ function localDateKey(value) {
70
+ const ms = timestampMs(value);
71
+ if (ms === null) return null;
72
+ const date = new Date(ms);
73
+ const year = date.getFullYear();
74
+ const month = String(date.getMonth() + 1).padStart(2, '0');
75
+ const day = String(date.getDate()).padStart(2, '0');
76
+ return `${year}-${month}-${day}`;
77
+ }
78
+
79
+ function taskDoneTime(task) {
80
+ if (task?.done_at) return task.done_at;
81
+ const events = Array.isArray(task?.events) ? task.events : [];
82
+ const done = [...events].reverse().find((event) => ['completed', 'done', 'accepted'].includes(
83
+ normalizeText(event?.event_type),
84
+ ));
85
+ return done?.created_at || null;
86
+ }
87
+
88
+ function readTaskMovement(root = process.cwd(), now = new Date()) {
89
+ const file = path.join(root, TASK_PROJECTION_FILE);
90
+ if (!fs.existsSync(file)) return { available: false, landed: 0, review: 0 };
91
+ try {
92
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
93
+ if (!Array.isArray(parsed?.tasks)) return { available: false, landed: 0, review: 0 };
94
+ const today = localDateKey(now);
95
+ return {
96
+ available: true,
97
+ landed: parsed.tasks.filter((task) => (
98
+ normalizeText(task?.status) === 'done'
99
+ && localDateKey(taskDoneTime(task)) === today
100
+ )).length,
101
+ review: parsed.tasks.filter((task) => normalizeText(task?.status) === 'review').length,
102
+ };
103
+ } catch {
104
+ return { available: false, landed: 0, review: 0 };
105
+ }
106
+ }
107
+
108
+ function readMissionMovement(root = process.cwd(), now = new Date()) {
109
+ const file = path.join(root, '.atris', 'state', 'missions.jsonl');
110
+ if (!fs.existsSync(file)) return { available: false, completed: 0 };
111
+ try {
112
+ const { listMissions } = require('./mission');
113
+ const today = localDateKey(now);
114
+ const completed = listMissions(root).filter((mission) => (
115
+ ['complete', 'completed'].includes(normalizeText(mission?.status))
116
+ && localDateKey(mission?.completed_at || mission?.updated_at) === today
117
+ )).length;
118
+ return { available: true, completed };
119
+ } catch {
120
+ return { available: false, completed: 0 };
121
+ }
122
+ }
123
+
124
+ function plural(count, singular) {
125
+ return `${count} ${singular}${count === 1 ? '' : 's'}`;
126
+ }
127
+
128
+ function todayText(tasks, missions) {
129
+ const parts = [];
130
+ if (tasks.available) {
131
+ parts.push(`${plural(tasks.landed, 'task')} landed`);
132
+ parts.push(`${tasks.review} waiting for your ok`);
133
+ }
134
+ if (missions.available) parts.push(`${plural(missions.completed, 'mission')} completed`);
135
+ return parts.length ? parts.join(', ') : 'no movement recorded today';
136
+ }
137
+
138
+ function buildGoalView(root = process.cwd(), now = new Date()) {
139
+ const scoreboard = readScoreboard(root);
140
+ const tasks = readTaskMovement(root, now);
141
+ const missions = readMissionMovement(root, now);
142
+ const today = todayText(tasks, missions);
143
+ const next = tasks.review > 0
144
+ ? 'atris task reviews'
145
+ : scoreboard.goal
146
+ ? 'atris task day'
147
+ : 'atris goal set "<sentence>"';
148
+ return {
149
+ goal: scoreboard.goal,
150
+ metrics: scoreboard.metrics,
151
+ today,
152
+ next,
153
+ };
154
+ }
155
+
156
+ function row(label, value) {
157
+ return ` ${label.padEnd(10)}${value}`;
158
+ }
159
+
160
+ function renderGoalView(view) {
161
+ const goal = view.goal || 'goal not set. set it: atris goal set "<sentence>"';
162
+ const lines = [row('goal', goal)];
163
+ if (view.metrics.length > 0) {
164
+ lines.push(row('distance', view.metrics.map((metric) => `${metric.name}: ${metric.value}`).join(' | ')));
165
+ }
166
+ lines.push(row('today', view.today));
167
+ lines.push(row('next', view.next));
168
+ return lines.join('\n');
169
+ }
170
+
171
+ function printHelp() {
172
+ console.log('usage: atris goal [--json]');
173
+ console.log(' atris goal set "<sentence>"');
174
+ console.log(' atris goal metric <name> <value>');
175
+ console.log(' atris goal metric <name> --rm');
176
+ }
177
+
178
+ function run(args = [], context = {}) {
179
+ const root = context.cwd || process.cwd();
180
+ const now = context.now || new Date();
181
+ const command = args[0];
182
+
183
+ try {
184
+ if (args.includes('--help') || args.includes('-h') || command === 'help') {
185
+ printHelp();
186
+ return 0;
187
+ }
188
+
189
+ if (command === 'set') {
190
+ const goal = normalizeText(args.slice(1).join(' '));
191
+ if (!goal) {
192
+ console.error('error: goal sentence is required');
193
+ return 2;
194
+ }
195
+ const scoreboard = readScoreboard(root);
196
+ writeScoreboard({ ...scoreboard, goal }, root);
197
+ console.log('goal set.');
198
+ return 0;
199
+ }
200
+
201
+ if (command === 'metric') {
202
+ const name = normalizeText(args[1]);
203
+ if (!name) {
204
+ console.error('error: metric name is required');
205
+ return 2;
206
+ }
207
+ const scoreboard = readScoreboard(root);
208
+ const metrics = scoreboard.metrics.filter((metric) => metric.name !== name);
209
+ if (args.includes('--rm')) {
210
+ writeScoreboard({ ...scoreboard, metrics }, root);
211
+ console.log(`metric ${name} removed.`);
212
+ return 0;
213
+ }
214
+ const value = normalizeText(args.slice(2).join(' '));
215
+ if (!value) {
216
+ console.error('error: metric value is required');
217
+ return 2;
218
+ }
219
+ metrics.push({ name, value });
220
+ writeScoreboard({ ...scoreboard, metrics }, root);
221
+ console.log(`metric ${name} set.`);
222
+ return 0;
223
+ }
224
+
225
+ if (command && command !== '--json') {
226
+ console.error(`error: unknown goal command: ${normalizeText(command)}`);
227
+ return 2;
228
+ }
229
+
230
+ const view = buildGoalView(root, now);
231
+ console.log(args.includes('--json') ? JSON.stringify(view, null, 2) : renderGoalView(view));
232
+ return 0;
233
+ } catch (error) {
234
+ console.error(`error: ${normalizeText(error?.message || error)}`);
235
+ return 2;
236
+ }
237
+ }
238
+
239
+ module.exports = {
240
+ SCOREBOARD_FILE,
241
+ buildGoalView,
242
+ readScoreboard,
243
+ renderGoalView,
244
+ run,
245
+ scoreboardPath,
246
+ writeScoreboard,
247
+ };