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
package/commands/gm.js CHANGED
@@ -6,6 +6,8 @@ const path = require('path');
6
6
 
7
7
  const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
8
8
  const AGENTXP_GLOBAL_SYNC_RULE = 'Run atris login, then sync. Owner-provided sync tokens are guided-demo fallback only.';
9
+ const GM_WATCH_DEFAULT_INTERVAL_SECONDS = 10;
10
+ const GM_WATCH_CONTENT_LIMIT = 180;
9
11
 
10
12
  const ROLE_PLAYERS_TO_IGNORE = new Set([
11
13
  'game-manager',
@@ -21,17 +23,21 @@ const ROLE_PLAYERS_TO_IGNORE = new Set([
21
23
  function showHelp() {
22
24
  console.log('');
23
25
  console.log('Usage: atris gm [--manager <id>] [--player <id>] [--workspace <path>] [--json]');
26
+ console.log(' atris gm --watch [--interval 10]');
24
27
  console.log('');
25
28
  console.log('Description:');
26
29
  console.log(' Enter AgentXP General Manager mode for the current Atris workspace.');
27
30
  console.log(' Shows local players, active missions, review queue, and the next command');
28
31
  console.log(' that moves the same AgentXP game loop forward.');
32
+ console.log(' Watch mode keeps that first board, then appends new mission, review, and feed events.');
29
33
  console.log('');
30
34
  console.log('Options:');
31
35
  console.log(' --manager <id> Manager id. Defaults to game-manager when present.');
32
36
  console.log(' --as <id> Alias for --player.');
33
37
  console.log(' --player <id> Preferred player when seeding a first local mission.');
34
38
  console.log(' --workspace <p> Read missions from another Atris workspace.');
39
+ console.log(' --watch Append new GM events until Ctrl+C.');
40
+ console.log(' --interval <sec> Watch polling interval. Defaults to 10.');
35
41
  console.log(' --no-seed Do not create a starter player mission.');
36
42
  console.log(' --json Print machine-readable mode state.');
37
43
  console.log(' --help, -h Show this help.');
@@ -294,17 +300,25 @@ function nextCommands({ seeded, reviewQueue, missions, players, manager }) {
294
300
  return ['atris gm --player <player>'];
295
301
  }
296
302
 
297
- function gmState(args = []) {
303
+ function readGmTaskRows(args = []) {
298
304
  const taskDb = require('../lib/task-db');
299
305
  const workspaceArg = flag(args, '--workspace') || flag(args, '--root') || process.cwd();
300
306
  const workspaceRoot = taskDb.workspaceRoot(path.resolve(workspaceArg));
301
307
  const db = taskDb.open();
302
- let tasks = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, {
308
+ const tasks = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, {
303
309
  workspaceRoot,
304
310
  limit: 500,
305
311
  }));
312
+ return { taskDb, db, workspaceRoot, tasks };
313
+ }
314
+
315
+ function gmState(args = [], options = {}) {
316
+ const { taskDb, db, workspaceRoot, tasks: initialTasks } = readGmTaskRows(args);
317
+ let tasks = initialTasks;
306
318
  const detected = inferManager(workspaceRoot, args);
307
- const starter = ensureStarterMission(taskDb, db, workspaceRoot, tasks, args);
319
+ const starter = options.seed === false
320
+ ? { tasks, seeded: null }
321
+ : ensureStarterMission(taskDb, db, workspaceRoot, tasks, args);
308
322
  tasks = starter.tasks;
309
323
 
310
324
  const missions = activeAgentXpTasks(tasks).map(compactTask);
@@ -337,6 +351,247 @@ function gmState(args = []) {
337
351
  };
338
352
  }
339
353
 
354
+ function watchIntervalSeconds(args = []) {
355
+ const raw = flag(args, '--interval');
356
+ const parsed = parseInt(raw || String(GM_WATCH_DEFAULT_INTERVAL_SECONDS), 10);
357
+ return Number.isFinite(parsed) ? Math.max(1, parsed) : GM_WATCH_DEFAULT_INTERVAL_SECONDS;
358
+ }
359
+
360
+ function watchStamp(value) {
361
+ const date = value ? new Date(value) : new Date();
362
+ const safe = Number.isFinite(date.getTime()) ? date : new Date();
363
+ return safe.toTimeString().slice(0, 5);
364
+ }
365
+
366
+ function eventMs(value) {
367
+ const ms = value ? Date.parse(value) : NaN;
368
+ return Number.isFinite(ms) ? ms : Date.now();
369
+ }
370
+
371
+ function watchLine(event) {
372
+ const name = String(event.name || event.kind || 'event').replace(/\s+/g, ' ').trim();
373
+ const content = clip(event.content || '', GM_WATCH_CONTENT_LIMIT);
374
+ return `[${watchStamp(event.at)}] [${event.kind}] ${name}: ${content}`;
375
+ }
376
+
377
+ function missionShortId(mission) {
378
+ const id = String(mission && mission.id || '');
379
+ if (!id) return 'mission';
380
+ return id.length > 20 ? `...${id.slice(-8)}` : id;
381
+ }
382
+
383
+ function missionFingerprint(mission) {
384
+ return [
385
+ mission && mission.status,
386
+ mission && mission.last_tick_at,
387
+ mission && mission.last_tick_index,
388
+ mission && mission.receipt_path,
389
+ ].join('|');
390
+ }
391
+
392
+ function missionEvent(mission) {
393
+ let heartbeat = '';
394
+ try {
395
+ const { missionHeartbeatLines } = require('./mission');
396
+ heartbeat = missionHeartbeatLines(mission).map(line => line.trim()).join(', ');
397
+ } catch {
398
+ heartbeat = '';
399
+ }
400
+ const title = mission.objective || mission.slug || mission.id || 'mission';
401
+ return {
402
+ id: `mission:${mission.id}`,
403
+ kind: 'mission',
404
+ name: mission.owner || mission.slug || 'mission',
405
+ at: mission.last_tick_at || mission.updated_at || mission.created_at,
406
+ content: [
407
+ missionShortId(mission),
408
+ `[${mission.status || 'unknown'}]`,
409
+ heartbeat || null,
410
+ clip(title, 100),
411
+ ].filter(Boolean).join(' - '),
412
+ fingerprint: missionFingerprint(mission),
413
+ };
414
+ }
415
+
416
+ function taskMissionFingerprint(task) {
417
+ return [
418
+ task && task.status,
419
+ task && task.updated_at,
420
+ task && task.claimed_by,
421
+ task && task.done_at,
422
+ ].join('|');
423
+ }
424
+
425
+ function taskMissionEvent(task) {
426
+ const player = taskAssignee(task) || task.claimed_by || 'unassigned';
427
+ return {
428
+ id: `task:${task.id}`,
429
+ kind: 'mission',
430
+ name: player,
431
+ at: task.updated_at ? new Date(Number(task.updated_at)).toISOString() : null,
432
+ content: `${taskRef(task)} [${task.status || 'unknown'}] ${task.title || 'untitled task'}`,
433
+ fingerprint: taskMissionFingerprint(task),
434
+ };
435
+ }
436
+
437
+ function reviewEvent(task) {
438
+ const player = task.assigned_to || taskAssignee(task) || task.claimed_by || 'review';
439
+ return {
440
+ id: task.id,
441
+ kind: 'review',
442
+ name: player,
443
+ at: task.updated_at ? new Date(Number(task.updated_at)).toISOString() : null,
444
+ content: `${task.ref || taskRef(task)} ready - ${task.title || 'untitled task'}`,
445
+ };
446
+ }
447
+
448
+ function feedPostId(post) {
449
+ return String(post.id || post.post_id || `${post.created_at || ''}:${post.author_id || ''}:${post.content || ''}`);
450
+ }
451
+
452
+ function credentialsFileExists() {
453
+ return fs.existsSync(path.join(os.homedir(), '.atris', 'credentials.json'));
454
+ }
455
+
456
+ async function fetchFeedEvents(workspaceRoot) {
457
+ if (!credentialsFileExists()) return [];
458
+ let feed;
459
+ let loadCredentials;
460
+ try {
461
+ feed = require('./feed');
462
+ loadCredentials = require('../utils/auth').loadCredentials;
463
+ } catch {
464
+ return [];
465
+ }
466
+ if (
467
+ typeof feed.findBusiness !== 'function'
468
+ || typeof feed.fetchPosts !== 'function'
469
+ || typeof feed.authorLabel !== 'function'
470
+ ) {
471
+ return [];
472
+ }
473
+ const business = feed.findBusiness(workspaceRoot);
474
+ if (!business) return [];
475
+ const credentials = loadCredentials();
476
+ if (!credentials || !credentials.token) return [];
477
+ const aliases = typeof feed.loadAuthorAliases === 'function' ? feed.loadAuthorAliases(business.root) : {};
478
+ const posts = await feed.fetchPosts(business.businessId, credentials.token, 20);
479
+ return posts.map((post) => {
480
+ const id = feedPostId(post);
481
+ return {
482
+ id,
483
+ kind: 'feed',
484
+ name: feed.authorLabel(post, aliases, credentials.user_id, credentials.email),
485
+ at: post.created_at,
486
+ content: String(post.content || '').replace(/\s+/g, ' ').trim(),
487
+ };
488
+ });
489
+ }
490
+
491
+ function collectLocalWatchState(args = []) {
492
+ const { workspaceRoot, tasks } = readGmTaskRows(args);
493
+ const taskMissions = activeAgentXpTasks(tasks);
494
+ const reviewQueue = taskMissions.filter(task => task.status === 'review');
495
+ return { workspaceRoot, taskMissions, reviewQueue };
496
+ }
497
+
498
+ function listDurableMissions(workspaceRoot) {
499
+ try {
500
+ const { listMissions } = require('./mission');
501
+ if (typeof listMissions !== 'function') return [];
502
+ return listMissions(workspaceRoot);
503
+ } catch {
504
+ return [];
505
+ }
506
+ }
507
+
508
+ async function gmWatchSnapshot(args) {
509
+ const local = collectLocalWatchState(args);
510
+ const durableMissions = listDurableMissions(local.workspaceRoot);
511
+ const feedEvents = await fetchFeedEvents(local.workspaceRoot);
512
+ return {
513
+ reviewEvents: local.reviewQueue.map(reviewEvent),
514
+ missionEvents: [
515
+ ...local.taskMissions.map(taskMissionEvent),
516
+ ...durableMissions.map(missionEvent),
517
+ ],
518
+ feedEvents,
519
+ };
520
+ }
521
+
522
+ function markSnapshotSeen(snapshot, seen) {
523
+ for (const event of snapshot.missionEvents) seen.missions.set(event.id, event.fingerprint);
524
+ for (const event of snapshot.reviewEvents) seen.reviews.add(event.id);
525
+ for (const event of snapshot.feedEvents) seen.feed.add(event.id);
526
+ }
527
+
528
+ function newWatchEvents(snapshot, seen) {
529
+ const events = [];
530
+ for (const event of snapshot.missionEvents) {
531
+ const previous = seen.missions.get(event.id);
532
+ if (previous !== event.fingerprint) {
533
+ events.push(event);
534
+ seen.missions.set(event.id, event.fingerprint);
535
+ }
536
+ }
537
+ for (const event of snapshot.reviewEvents) {
538
+ if (!seen.reviews.has(event.id)) {
539
+ events.push(event);
540
+ seen.reviews.add(event.id);
541
+ }
542
+ }
543
+ for (const event of snapshot.feedEvents) {
544
+ if (!seen.feed.has(event.id)) {
545
+ events.push(event);
546
+ seen.feed.add(event.id);
547
+ }
548
+ }
549
+ return events.sort((a, b) => eventMs(a.at) - eventMs(b.at));
550
+ }
551
+
552
+ async function gmWatch(args = []) {
553
+ const intervalSeconds = watchIntervalSeconds(args);
554
+ const seen = {
555
+ missions: new Map(),
556
+ reviews: new Set(),
557
+ feed: new Set(),
558
+ };
559
+ try {
560
+ markSnapshotSeen(await gmWatchSnapshot(args), seen);
561
+ } catch {
562
+ // Watch mode is append-only; transient read failures should not redraw or exit.
563
+ }
564
+
565
+ console.log('');
566
+ console.log(`Watching GM events every ${intervalSeconds}s. Ctrl+C to stop.`);
567
+ console.log('');
568
+
569
+ let polling = false;
570
+ const poll = async () => {
571
+ if (polling) return;
572
+ polling = true;
573
+ try {
574
+ const events = newWatchEvents(await gmWatchSnapshot(args, seen), seen);
575
+ for (const event of events) console.log(watchLine(event));
576
+ } catch {
577
+ // Match fleet watch: quiet on transient poll failures.
578
+ } finally {
579
+ polling = false;
580
+ }
581
+ };
582
+
583
+ const interval = setInterval(poll, intervalSeconds * 1000);
584
+ return new Promise((resolve) => {
585
+ const onSigint = () => {
586
+ clearInterval(interval);
587
+ process.off('SIGINT', onSigint);
588
+ console.log('\nStopped watching.\n');
589
+ resolve();
590
+ };
591
+ process.on('SIGINT', onSigint);
592
+ });
593
+ }
594
+
340
595
  function render(state) {
341
596
  console.log('');
342
597
  console.log('AgentXP General Manager');
@@ -374,21 +629,48 @@ function render(state) {
374
629
  for (const command of state.next_commands) console.log(`- ${command}`);
375
630
  }
376
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
+
377
643
  async function gmCommand(...args) {
378
644
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
379
645
  showHelp();
380
646
  return;
381
647
  }
382
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
+
383
661
  const state = gmState(args);
384
662
  if (hasFlag(args, '--json')) {
385
663
  console.log(JSON.stringify(state, null, 2));
386
664
  return;
387
665
  }
388
666
  render(state);
667
+ if (hasFlag(args, '--watch')) {
668
+ await gmWatch(args);
669
+ }
389
670
  }
390
671
 
391
672
  module.exports = {
392
673
  gmCommand,
393
674
  gmState,
675
+ gmWatchSnapshot,
394
676
  };
@@ -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
+ };