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/radar.js CHANGED
@@ -100,6 +100,7 @@ function collectAgents(deps) {
100
100
  return {
101
101
  pid: row.pid,
102
102
  agent: row.agent,
103
+ command: row.command,
103
104
  status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
104
105
  cwd,
105
106
  repo: repoLabel(cwd),
@@ -148,7 +149,8 @@ function loadTasksCached(root, deps, cache) {
148
149
  return cache.get(root);
149
150
  }
150
151
 
151
- function untaskedReason(agent, taskWorkspaceRoot, tasks) {
152
+ function untaskedReason(agent, taskWorkspaceRoot, tasks, binding = null) {
153
+ if (binding?.interactive) return 'interactive session';
152
154
  if (!agent.cwd) return 'cwd unknown';
153
155
  if (!taskWorkspaceRoot) return 'no task projection';
154
156
  if (!tasks.length) return 'empty task projection';
@@ -159,10 +161,11 @@ function shellQuote(value) {
159
161
  return `'${String(value || '').replace(/'/g, "'\\''")}'`;
160
162
  }
161
163
 
162
- function untaskedAction(agent, taskWorkspaceRoot, tasks) {
163
- const reason = untaskedReason(agent, taskWorkspaceRoot, tasks);
164
+ function untaskedAction(agent, taskWorkspaceRoot, tasks, binding = null) {
165
+ const reason = untaskedReason(agent, taskWorkspaceRoot, tasks, binding);
164
166
  const pid = agent.pid || '?';
165
167
  const actor = agent.agent || 'agent';
168
+ if (reason === 'interactive session') return `inspect pid ${pid} interactive claude session; bind via member lock, worktree sidecar, or branch identity before assuming a repo task`;
166
169
  if (reason === 'cwd unknown') return `inspect pid ${pid} cwd with lsof`;
167
170
  if (reason === 'no active task') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task next --as ${actor}`;
168
171
  if (reason === 'empty task projection') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task new "<small concrete title>" --tag ops`;
@@ -410,6 +413,213 @@ function taskRef(task) {
410
413
  return task ? (task.display_id || task.legacy_ref || task.id || '-') : '-';
411
414
  }
412
415
 
416
+ function isInteractiveClaudeCommand(command) {
417
+ const cmd = String(command || '');
418
+ if (!/(^|\s|\/)claude(\s|$)/.test(cmd) || /Claude\.app/.test(cmd)) return false;
419
+ return !/(^|\s)-p(\s|$)/.test(cmd) && !/(^|\s)--print(\s|$)/.test(cmd);
420
+ }
421
+
422
+ function parseSessionIdFromCommand(command) {
423
+ const parts = String(command || '').split(/\s+/);
424
+ for (let i = 0; i < parts.length; i += 1) {
425
+ if ((parts[i] === '--session-id' || parts[i] === '--resume') && parts[i + 1]) return parts[i + 1];
426
+ const inline = parts[i].match(/^--session-id=(.+)$/);
427
+ if (inline) return inline[1];
428
+ }
429
+ return null;
430
+ }
431
+
432
+ function loadAgentWorktreeSidecar(cwd, deps) {
433
+ if (!cwd) return null;
434
+ let current = path.resolve(cwd);
435
+ for (let depth = 0; depth < 8; depth += 1) {
436
+ const payload = readJsonFile(path.join(current, '.atris', 'agent-worktree.json'), deps, null);
437
+ if (payload) return payload;
438
+ const parent = path.dirname(current);
439
+ if (!parent || parent === current) break;
440
+ current = parent;
441
+ }
442
+ return null;
443
+ }
444
+
445
+ function branchIdentityAtCwd(cwd, execFile) {
446
+ if (!cwd) return null;
447
+ try {
448
+ const branch = String(execFile('git', ['-C', cwd, 'branch', '--show-current'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
449
+ if (!branch) return null;
450
+ const owner = String(execFile('git', ['-C', cwd, 'config', '--get', `branch.${branch}.atris-owner`], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
451
+ const taskHint = String(execFile('git', ['-C', cwd, 'config', '--get', `branch.${branch}.atris-task`], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
452
+ if (!owner) return null;
453
+ return { owner, task_hint: taskHint || null, branch };
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+
459
+ function loadMissionLocks(stateDir, deps) {
460
+ const locks = [];
461
+ for (const name of listNames(stateDir, deps)) {
462
+ const match = name.match(/^mission-(.+)\.lock$/);
463
+ if (!match) continue;
464
+ const payload = readJsonFile(path.join(stateDir, name), deps, null);
465
+ if (!payload || payload.pid == null) continue;
466
+ locks.push({
467
+ ...payload,
468
+ mission_id: payload.mission_id || match[1],
469
+ lock_file: name,
470
+ });
471
+ }
472
+ return locks;
473
+ }
474
+
475
+ function loadMemberLoopLocks(stateDir, deps) {
476
+ const dir = path.join(stateDir, 'member-loops');
477
+ const locks = [];
478
+ for (const name of listNames(dir, deps)) {
479
+ if (!name.endsWith('.lock.json')) continue;
480
+ const payload = readJsonFile(path.join(dir, name), deps, null);
481
+ if (!payload || payload.pid == null) continue;
482
+ locks.push({
483
+ ...payload,
484
+ member: payload.member || name.replace(/\.lock\.json$/, ''),
485
+ });
486
+ }
487
+ return locks;
488
+ }
489
+
490
+ function loadWorkspaceIdentity(root, deps, cache, nowMs) {
491
+ if (!root) return { missions: [], missionLocks: [], memberLocks: [] };
492
+ if (cache.has(root)) return cache.get(root);
493
+ const stateDir = path.join(root, '.atris', 'state');
494
+ const bundle = {
495
+ missions: loadMissions(root, deps, nowMs),
496
+ missionLocks: loadMissionLocks(stateDir, deps),
497
+ memberLocks: loadMemberLoopLocks(stateDir, deps),
498
+ };
499
+ cache.set(root, bundle);
500
+ return bundle;
501
+ }
502
+
503
+ function taskOwnerValues(task) {
504
+ return [
505
+ task?.assigned_to,
506
+ task?.claimed_by,
507
+ task?.metadata?.assigned_to,
508
+ task?.metadata?.claimed_by,
509
+ task?.metadata?.owner,
510
+ ].filter(Boolean).map(value => String(value));
511
+ }
512
+
513
+ function taskMatchesOwner(task, owner) {
514
+ if (!task || !owner) return false;
515
+ const want = String(owner).toLowerCase();
516
+ return taskOwnerValues(task).some(value => value.toLowerCase() === want);
517
+ }
518
+
519
+ function findTaskByRef(tasks, ref) {
520
+ if (!ref) return null;
521
+ return tasks.find(task => taskRef(task) === ref) || null;
522
+ }
523
+
524
+ function taskForOwner(tasks, owner, statuses = ['claimed', 'open', 'review']) {
525
+ if (!owner) return null;
526
+ for (const status of statuses) {
527
+ const matches = tasks.filter(task => task.status === status && taskMatchesOwner(task, owner));
528
+ if (matches.length === 1) return matches[0];
529
+ if (matches.length > 1) return matches[0];
530
+ }
531
+ return null;
532
+ }
533
+
534
+ function resolveAgentTaskBinding(context) {
535
+ const {
536
+ agent,
537
+ tasks,
538
+ missions,
539
+ missionLocks,
540
+ memberLocks,
541
+ sidecar,
542
+ branchIdentity,
543
+ sessionId,
544
+ interactive,
545
+ taskWorkspaceRoot,
546
+ } = context;
547
+ const pid = String(agent?.pid || '');
548
+
549
+ const missionLock = missionLocks.find(row => String(row.pid) === pid);
550
+ if (missionLock) {
551
+ const mission = missions.find(row => row.id === missionLock.mission_id);
552
+ const task = findTaskByRef(tasks, mission?.task_spine?.task_ref) || taskForOwner(tasks, mission?.owner);
553
+ return {
554
+ task,
555
+ owner: mission?.owner || ownerForTask(task),
556
+ member: mission?.owner || sidecar?.member || null,
557
+ session_id: mission?.claude_session_id || sessionId || null,
558
+ mission_id: mission?.id || missionLock.mission_id,
559
+ ...taskBinding('mission_lock', 'process'),
560
+ };
561
+ }
562
+
563
+ const memberLock = memberLocks.find(row => String(row.pid) === pid);
564
+ if (memberLock) {
565
+ const task = taskForOwner(tasks, memberLock.member);
566
+ return {
567
+ task,
568
+ owner: memberLock.member,
569
+ member: memberLock.member,
570
+ session_id: sessionId || null,
571
+ ...taskBinding('member_loop_lock', 'process'),
572
+ };
573
+ }
574
+
575
+ if (sessionId) {
576
+ const mission = missions.find(row => row.claude_session_id === sessionId || row.pending_session_id === sessionId);
577
+ if (mission) {
578
+ const task = findTaskByRef(tasks, mission.task_spine?.task_ref) || taskForOwner(tasks, mission.owner);
579
+ return {
580
+ task,
581
+ owner: mission.owner || ownerForTask(task),
582
+ member: mission.owner || sidecar?.member || null,
583
+ session_id: sessionId,
584
+ mission_id: mission.id,
585
+ ...taskBinding('claude_session', 'process'),
586
+ };
587
+ }
588
+ }
589
+
590
+ const ownerHint = sidecar?.owner || sidecar?.member || branchIdentity?.owner || null;
591
+ if (ownerHint) {
592
+ const task = taskForOwner(tasks, ownerHint);
593
+ return {
594
+ task,
595
+ owner: ownerHint,
596
+ member: sidecar?.member || ownerHint,
597
+ session_id: sessionId || null,
598
+ ...taskBinding(sidecar ? 'agent_worktree' : 'branch_identity', 'process'),
599
+ };
600
+ }
601
+
602
+ if (interactive) {
603
+ return {
604
+ task: null,
605
+ owner: 'interactive',
606
+ member: null,
607
+ session_id: sessionId || null,
608
+ interactive: true,
609
+ ...taskBinding('interactive_session', 'process'),
610
+ };
611
+ }
612
+
613
+ const fallbackTask = taskForCwd(tasks, agent?.cwd, taskWorkspaceRoot, agent);
614
+ return {
615
+ task: fallbackTask,
616
+ owner: ownerForTask(fallbackTask),
617
+ member: null,
618
+ session_id: sessionId || null,
619
+ ...(fallbackTask ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null)),
620
+ };
621
+ }
622
+
413
623
  function taskOwnerMatchesAgent(task, agent) {
414
624
  const actor = String(agent?.agent || '').toLowerCase();
415
625
  if (!actor) return false;
@@ -438,15 +648,22 @@ function taskForCwd(tasks, cwd, workspaceRoot = cwd, agent = null) {
438
648
  || null;
439
649
  }
440
650
 
651
+ function taskBinding(source, scope = null) {
652
+ if (!source) return { task_source: null, task_scope: null };
653
+ return { task_source: source, task_scope: scope };
654
+ }
655
+
441
656
  function taskBindingForProjection(task) {
442
- if (!task) return { task_source: null, task_scope: null };
443
- return {
444
- task_source: 'repo_task_projection',
445
- task_scope: 'repo',
446
- };
657
+ return task ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null);
447
658
  }
448
659
 
449
660
  function taskSourceLabel(sources = []) {
661
+ if (sources.includes('member_loop_lock')) return 'member loop lock';
662
+ if (sources.includes('mission_lock')) return 'mission lock';
663
+ if (sources.includes('claude_session')) return 'claude session id';
664
+ if (sources.includes('agent_worktree')) return 'agent worktree sidecar';
665
+ if (sources.includes('branch_identity')) return 'branch identity';
666
+ if (sources.includes('interactive_session')) return 'interactive session';
450
667
  if (sources.includes('repo_task_projection')) return 'repo projection; verify ownership';
451
668
  return sources.filter(Boolean).join(', ');
452
669
  }
@@ -556,23 +773,45 @@ function collectRadar(options = {}) {
556
773
  const nowMs = options.nowMs || Date.now();
557
774
  const tasks = loadTasks(root, deps);
558
775
  const taskCache = new Map([[root, tasks]]);
776
+ const identityCache = new Map();
559
777
  const missions = loadMissions(root, deps, nowMs);
778
+ identityCache.set(root, { missions, missionLocks: loadMissionLocks(path.join(root, '.atris', 'state'), deps), memberLocks: loadMemberLoopLocks(path.join(root, '.atris', 'state'), deps) });
560
779
  const worktrees = loadWorktrees(root, deps);
561
780
  const agents = collectAgents(deps).map(agent => {
562
781
  const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
563
782
  const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
564
- const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot, agent);
565
- const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
566
- const taskBinding = taskBindingForProjection(task);
783
+ const identity = taskWorkspaceRoot ? loadWorkspaceIdentity(taskWorkspaceRoot, deps, identityCache, nowMs) : { missions: [], missionLocks: [], memberLocks: [] };
784
+ const sidecar = loadAgentWorktreeSidecar(agent.cwd, deps);
785
+ const branchIdentity = branchIdentityAtCwd(agent.cwd, deps.execFile);
786
+ const sessionId = parseSessionIdFromCommand(agent.command);
787
+ const interactive = agent.agent === 'claude' && isInteractiveClaudeCommand(agent.command);
788
+ const resolved = resolveAgentTaskBinding({
789
+ agent,
790
+ tasks: agentTasks,
791
+ missions: identity.missions,
792
+ missionLocks: identity.missionLocks,
793
+ memberLocks: identity.memberLocks,
794
+ sidecar,
795
+ branchIdentity,
796
+ sessionId,
797
+ interactive,
798
+ taskWorkspaceRoot,
799
+ });
800
+ const task = resolved.task;
801
+ const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks, resolved);
802
+ const binding = { task_source: resolved.task_source, task_scope: resolved.task_scope };
567
803
  return {
568
804
  ...agent,
569
805
  task: taskRef(task),
570
806
  task_status: task?.status || null,
571
- owner: ownerForTask(task),
807
+ owner: resolved.owner || ownerForTask(task),
808
+ member: resolved.member || null,
809
+ session_id: resolved.session_id || null,
810
+ mission_id: resolved.mission_id || null,
572
811
  task_workspace: taskWorkspaceRoot ? repoLabel(taskWorkspaceRoot) : null,
573
- ...taskBinding,
812
+ ...binding,
574
813
  task_reason: taskReason,
575
- task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
814
+ task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks, resolved),
576
815
  };
577
816
  });
578
817
  const osState = {
@@ -949,10 +1188,16 @@ function radarCommand(args = [], options = {}) {
949
1188
  module.exports = {
950
1189
  agentTypeForCommand,
951
1190
  agentTopPayload,
1191
+ branchIdentityAtCwd,
952
1192
  collectRadar,
1193
+ isInteractiveClaudeCommand,
1194
+ loadAgentWorktreeSidecar,
953
1195
  parsePsOutput,
1196
+ parseSessionIdFromCommand,
954
1197
  parseWorktrees,
955
1198
  radarCommand,
956
1199
  renderAgentTop,
957
1200
  renderRadar,
1201
+ resolveAgentTaskBinding,
1202
+ taskForOwner,
958
1203
  };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * atris rainmaker — passthrough to atrisos-backend/scripts/rainmaker.py
5
+ *
6
+ * atris rainmaker # ASCII status dashboard
7
+ * atris rainmaker instinct # one morning opinion
8
+ * atris rainmaker batting # accuracy scorecard
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { spawnSync } = require('child_process');
15
+
16
+ function resolveBackendRoot() {
17
+ const candidates = [
18
+ process.env.ATRIS_BACKEND_DIR,
19
+ path.resolve(__dirname, '..', '..', 'atrisos-backend'),
20
+ path.resolve(process.cwd(), '..', 'atrisos-backend'),
21
+ path.join(os.homedir(), 'arena', 'atrisos-backend'),
22
+ ].filter(Boolean);
23
+ for (const root of candidates) {
24
+ if (fs.existsSync(path.join(root, 'scripts', 'rainmaker.py'))) return root;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ function rainmakerCommand(args = []) {
30
+ if (args[0] === '--help' || args[0] === '-h') {
31
+ console.log('Usage: atris rainmaker [args...]\n\nPassthrough to python3 scripts/rainmaker.py in atrisos-backend.');
32
+ return 0;
33
+ }
34
+ const root = resolveBackendRoot();
35
+ if (!root) {
36
+ console.error('Cannot find scripts/rainmaker.py. Set ATRIS_BACKEND_DIR to atrisos-backend.');
37
+ return 1;
38
+ }
39
+ const script = path.join(root, 'scripts', 'rainmaker.py');
40
+ const py = process.env.ATRIS_PYTHON || 'python3';
41
+ const result = spawnSync(py, [script, ...args], { cwd: root, stdio: 'inherit', env: process.env });
42
+ if (result.error) {
43
+ console.error(`Failed to spawn ${py}: ${result.error.message}`);
44
+ return 1;
45
+ }
46
+ return result.status ?? 1;
47
+ }
48
+
49
+ module.exports = { rainmakerCommand, resolveBackendRoot };