atris 3.37.0 → 3.38.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 (139) hide show
  1. package/README.md +2 -0
  2. package/atris/PERSONA.md +3 -7
  3. package/atris.md +10 -2
  4. package/ax +158 -33
  5. package/bin/atris.js +49 -2
  6. package/commands/activate.js +16 -3
  7. package/commands/agent-spawn.js +0 -7
  8. package/commands/autoland.js +92 -14
  9. package/commands/autopilot-front.js +0 -4
  10. package/commands/autopilot.js +0 -12
  11. package/commands/brain.js +0 -7
  12. package/commands/brief.js +0 -2
  13. package/commands/business-sync.js +0 -1
  14. package/commands/business.js +107 -12
  15. package/commands/clean.js +31 -2
  16. package/commands/close.js +46 -11
  17. package/commands/cloud.js +0 -1
  18. package/commands/codex-goal.js +0 -2
  19. package/commands/computer.js +6 -5
  20. package/commands/console.js +46 -9
  21. package/commands/deck.js +29 -8
  22. package/commands/dream.js +0 -7
  23. package/commands/drill.js +0 -1
  24. package/commands/engine.js +10 -18
  25. package/commands/experiments.js +0 -4
  26. package/commands/fleet-report.js +12 -4
  27. package/commands/game.js +261 -0
  28. package/commands/gm.js +10 -2
  29. package/commands/goal.js +0 -6
  30. package/commands/harvest.js +0 -1
  31. package/commands/improve.js +29 -8
  32. package/commands/init.js +18 -34
  33. package/commands/integrations.js +0 -7
  34. package/commands/land.js +71 -8
  35. package/commands/launchpad.js +0 -2
  36. package/commands/lesson.js +17 -7
  37. package/commands/loop-front.js +0 -2
  38. package/commands/loops.js +6 -5
  39. package/commands/meet.js +0 -2
  40. package/commands/member.js +68 -3
  41. package/commands/mission.js +359 -102
  42. package/commands/next.js +0 -1
  43. package/commands/now.js +0 -12
  44. package/commands/one-lap.js +8 -8
  45. package/commands/orb.js +0 -4
  46. package/commands/pack-craft.js +0 -1
  47. package/commands/pack.js +670 -77
  48. package/commands/play.js +0 -2
  49. package/commands/probe.js +10 -4
  50. package/commands/pulse.js +73 -48
  51. package/commands/push.js +0 -4
  52. package/commands/radar.js +14 -11
  53. package/commands/recover.js +340 -0
  54. package/commands/report.js +0 -1
  55. package/commands/router.js +319 -0
  56. package/commands/run-front.js +0 -2
  57. package/commands/search.js +0 -3
  58. package/commands/skill.js +7 -2
  59. package/commands/slop.js +225 -7
  60. package/commands/status.js +6 -0
  61. package/commands/stream.js +0 -2
  62. package/commands/study.js +0 -3
  63. package/commands/sync-checkout.js +51 -0
  64. package/commands/sync.js +11 -40
  65. package/commands/task.js +125 -39
  66. package/commands/teach.js +86 -0
  67. package/commands/unknowns.js +0 -10
  68. package/commands/update.js +0 -1
  69. package/commands/visualize.js +0 -5
  70. package/commands/watch.js +0 -6
  71. package/commands/wiki.js +41 -11
  72. package/commands/wish.js +0 -1
  73. package/commands/worktree.js +87 -14
  74. package/commands/xp.js +1 -6
  75. package/commands/youtube.js +0 -2
  76. package/lib/accept-verify-gate.js +122 -0
  77. package/lib/activity-stream.js +0 -2
  78. package/lib/auto-accept-certified.js +145 -2
  79. package/lib/autoland.js +0 -3
  80. package/lib/ax-auto-lane.js +94 -10
  81. package/lib/bench/context.js +0 -2
  82. package/lib/bench/report.js +0 -1
  83. package/lib/bench/runner.js +0 -11
  84. package/lib/brief-ledger.js +0 -6
  85. package/lib/business-simulate.js +382 -0
  86. package/lib/chat-log-scan.js +0 -5
  87. package/lib/checkout-sync.js +52 -0
  88. package/lib/claude-boot-block.js +48 -0
  89. package/lib/cloud-mission.js +0 -6
  90. package/lib/codex-flight.js +0 -1
  91. package/lib/conductor-artifacts.js +25 -0
  92. package/lib/context-gatherer.js +0 -1
  93. package/lib/deck-compose.js +0 -1
  94. package/lib/deck-history.js +0 -1
  95. package/lib/deck-layout.js +0 -5
  96. package/lib/deck-review.js +0 -7
  97. package/lib/deck-schema.js +0 -1
  98. package/lib/default-verifier.js +0 -2
  99. package/lib/engine-registry.js +16 -8
  100. package/lib/experiments/daily.js +0 -6
  101. package/lib/fleet.js +210 -27
  102. package/lib/functional-owner.js +0 -3
  103. package/lib/inspect-fields.js +0 -1
  104. package/lib/known-commands.js +2 -2
  105. package/lib/learnings.js +0 -2
  106. package/lib/loop-doctor.js +11 -17
  107. package/lib/manifest.js +0 -2
  108. package/lib/member-alive.js +0 -1
  109. package/lib/member-switches.js +0 -5
  110. package/lib/mission-artifact.js +0 -1
  111. package/lib/mission-protected-lane.js +293 -0
  112. package/lib/mission-room.js +0 -2
  113. package/lib/mission-runtime-loop.js +0 -1
  114. package/lib/next-moves.js +0 -18
  115. package/lib/official-cli-integration.js +0 -4
  116. package/lib/orb-context.js +6 -3
  117. package/lib/orb-scorecard.js +0 -1
  118. package/lib/outbound-send-gate.js +0 -6
  119. package/lib/permission-grants.js +0 -6
  120. package/lib/policy-lessons.js +0 -2
  121. package/lib/pulse.js +68 -15
  122. package/lib/review-integrity.js +0 -2
  123. package/lib/router-brain.js +0 -2
  124. package/lib/runner-command.js +0 -4
  125. package/lib/runtime-bootstrap.js +0 -1
  126. package/lib/scorecard.js +0 -1
  127. package/lib/self-drive.js +1 -1
  128. package/lib/state-detection.js +0 -6
  129. package/lib/task-db.js +0 -4
  130. package/lib/team-presence.js +0 -1
  131. package/lib/voice-gate.js +63 -2
  132. package/lib/wiki.js +152 -9
  133. package/lib/wish-audit.js +20 -6
  134. package/lib/wish-delegate.js +26 -20
  135. package/lib/wish-stats.js +0 -5
  136. package/lib/wish-store.js +0 -1
  137. package/lib/workspace-safety.js +0 -2
  138. package/lib/zip.js +0 -1
  139. package/package.json +1 -1
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
3
7
  const SHORT_LOOKUP_MAX_CHARS = 140;
4
8
  const LONG_INPUT_MIN_CHARS = 1200;
5
9
  const TINY_MESSAGE_MAX_CHARS = 20;
@@ -9,30 +13,77 @@ const TRADEOFF_DEPTH_MIN_CHARS = 100;
9
13
 
10
14
  // Anchored to the start: a lookup word mid-sentence is usually a subordinate
11
15
  // clause (my fan spins when i run tests), not a lookup.
12
- const LOOKUP_START_RE = /^(?:(?:please|pls|hey|hi|yo)[,: ]+)?(?:whats|whos|wheres|what|who|when|where|list|show|define)\b/i;
16
+ const LOOKUP_START_RE = /^(?:(?:please|pls|hey|hi|yo)[,: ]+)?(?:whats|whos|wheres|whens|what|who|when|where|list|show|define)\b/i;
13
17
  // Words that turn a lookup-shaped message into a judgment call: never fast.
14
- const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth|good way)\b/i;
18
+ const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth|good way)\b|\b(?:legal|compliance|gdpr|lawsuit|liability|subpoena)\b/i;
15
19
  const HEAVY_REASONING_RE = /\b(?:design(?:s|ed|ing)?|architect(?:s|ed|ing)?|prov(?:e|es|ed|ing)|analy(?:ze|zes|zed|zing|se|ses|sed|sing)|compar(?:e|es|ed|ing)|plan(?:s|ned|ning)?|sketch(?:es|ed|ing)?)\b|\bwalk(?:s|ed|ing)? (?:me |us )?through\b/i;
16
20
  const DECISION_PHRASE_RE = /\b(?:should|whether|decide|deciding)\b/i;
17
- const ARGUE_BOTH_RE = /\bargue\b|\bboth sides\b|\bsteelman\b/i;
21
+ const ARGUE_BOTH_RE = /\bargue\b|\bboth sides\b|\bsteelman\b|\bmath it out\b|\bdo the math\b|\bshow your math\b/i;
18
22
  const CODE_EDIT_RE = /\b(?:fix(?:es|ed|ing)?|refactor(?:s|ed|ing)?|renam(?:e|es|ed|ing)|debug(?:s|ged|ging)?)\b/i;
19
- const CODE_DIAGNOSIS_RE = /\bwhy\b|\bwrong\b|\bfail(?:s|ed|ing)?\b|\bresolv(?:e|es|ed|ing)?\b|\bbroken\b|\bnot work/i;
20
- const CODE_CONTEXT_RE = /```|(?:^|\n)\s*(?:traceback\b|(?:[a-z]+)?(?:error|exception):|at\s+\S+\s+\()/im;
23
+ const CODE_DIAGNOSIS_RE = /\bwhy\b|\bwrong\b|\bbug\b|\bfail(?:s|ed|ing)?\b|\bresolv(?:e|es|ed|ing)?\b|\bbroken\b|\bnot work/i;
24
+ const CODE_CONTEXT_RE = /```|\bregexp?\b|(?:^|\n)\s*(?:traceback\b|(?:[a-z]+)?(?:error|exception):|at\s+\S+\s+\()/im;
21
25
  const ERROR_EXPLAIN_RE = /\bexplain (?:this|the) error\b|\bwhat does (?:this|the) error mean\b/i;
22
26
  // Yes/no capability checks are lookups even without a lookup keyword.
23
27
  const YESNO_LOOKUP_RE = /^(?:does|is|are|can|do)\b/i;
28
+ // Tiny how-do-i asks are muscle-memory lookups; longer ones are advice.
29
+ const HOWTO_LOOKUP_RE = /^how (?:do|does|did) (?:i|we|you)\b/i;
30
+ const HOWTO_LOOKUP_MAX_CHARS = 40;
24
31
  const YESNO_LOOKUP_MAX_CHARS = 80;
32
+ // Incidents and legal surfaces are stakes, not size: never route them cheap.
33
+ const INCIDENT_RE = /\b(?:prod(?:uction)? is down|outage|everything (?:500|about:blank|fail)|users can'?t (?:login|log in)|data loss|site is down for everyone)\b/i;
34
+ const LEGAL_DRAFT_RE = /\b(?:terms of service|privacy policy|\bdpa\b|contract)\b/i;
35
+ const STAKES_WORD_RE = /\b(?:legal|compliance|gdpr|lawsuit|liability|subpoena)\b/i;
36
+ // Abstract topics have no cheap correct answer even when the ask is short.
37
+ const DEEP_TOPIC_RE = /\b(?:consciousness|free will|sentien\w*|computab\w*|meaning of life|the universe|morality|ethics of)\b/i;
25
38
  // Troubleshooting language means advice, not recall: keep it out of fast.
26
39
  const TROUBLE_RE = /\bfail(?:s|ed|ing)?\b|\bcrash(?:es|ed|ing)?\b|\bbroken\b|\bnot working\b/i;
27
- const WEIGHING_WORD_RE = /\b(?:consider|weigh|rank|estimate)\b/i;
40
+ const WEIGHING_WORD_RE = /\b(?:consider|weigh|rank|estimate|think (?:about|through))\b/i;
28
41
  const WEIGHING_FACTOR_MIN_COMMAS = 2;
29
42
  const TRANSFORM_START_RE = /^(?:convert|summarize|translate)\b/i;
43
+ // Reshaping pasted content is editing work, not analysis; question marks
44
+ // inside the paste do not make it a multi-question ask.
45
+ const TRANSFORM_INTENT_RE = /^(?:turn|clean up|rewrite|reword|compress|tighten|polish)\b/i;
46
+ // Possessives point at workspace context the model must gather first.
47
+ const OWN_CONTEXT_RE = /\b(?:our|my)\b/i;
48
+ const SPANISH_LOOKUP_RE = /^(?:que|qu\u00e9|cual|cu\u00e1l|quien|qui\u00e9n|cuando|cu\u00e1ndo|donde|d\u00f3nde|como|c\u00f3mo)\b/i;
30
49
  const GREETING_RE = /^(?:hey|hi|hello|yo|thanks|thank you|ok|okay|cool|nice|got it|great|perfect|yep|no worries)\b/i;
31
50
 
32
51
  function threshold(value, fallback) {
33
52
  return Number.isFinite(value) && value >= 0 ? value : fallback;
34
53
  }
35
54
 
55
+ function words(text) {
56
+ return String(text || '').toLowerCase().match(/[a-z0-9]+/g) || [];
57
+ }
58
+
59
+ function overrideMatches(text, override) {
60
+ if (!override || typeof override !== 'object' || !override.id || !override.test
61
+ || !override.lane || !override.reason) return false;
62
+ const test = override.test;
63
+ const normalized = text.toLowerCase();
64
+ if (test.startsWith !== undefined
65
+ && !normalized.startsWith(String(test.startsWith).toLowerCase())) return false;
66
+ if (Array.isArray(test.includesAll)) {
67
+ const messageWords = new Set(words(text));
68
+ if (!test.includesAll.every((word) => messageWords.has(String(word).toLowerCase()))) return false;
69
+ }
70
+ if (Number.isFinite(test.maxChars) && text.length > test.maxChars) return false;
71
+ if (Number.isFinite(test.minChars) && text.length < test.minChars) return false;
72
+ return true;
73
+ }
74
+
75
+ function loadOverrides(filePath) {
76
+ const resolved = filePath
77
+ || process.env.ATRIS_ROUTER_OVERRIDES_PATH
78
+ || path.join(os.homedir(), '.atris', 'router-overrides.json');
79
+ try {
80
+ const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
81
+ return Array.isArray(parsed) ? parsed : [];
82
+ } catch (_) {
83
+ return [];
84
+ }
85
+ }
86
+
36
87
  function pickLane(message, opts = {}) {
37
88
  const text = String(message || '').trim();
38
89
  const shortLookupMaxChars = threshold(opts.shortLookupMaxChars, SHORT_LOOKUP_MAX_CHARS);
@@ -41,9 +92,23 @@ function pickLane(message, opts = {}) {
41
92
  const commaCount = (text.match(/,/g) || []).length;
42
93
  const hasCodeContext = CODE_CONTEXT_RE.test(text);
43
94
 
95
+ const overrides = Array.isArray(opts.overrides) ? opts.overrides : [];
96
+ for (const override of overrides) {
97
+ if (overrideMatches(text, override)) {
98
+ return {
99
+ lane: override.lane,
100
+ reason: override.reason,
101
+ override_id: override.id,
102
+ };
103
+ }
104
+ }
105
+
44
106
  if (text.length > longInputMinChars) {
45
107
  return { lane: 'max', reason: 'max fits this long input.' };
46
108
  }
109
+ if (TRANSFORM_INTENT_RE.test(text) && !HEAVY_REASONING_RE.test(text)) {
110
+ return { lane: 'pro', reason: 'pro fits reshaping the content you pasted.' };
111
+ }
47
112
  if (questionCount > 1) {
48
113
  return { lane: 'max', reason: 'max fits a request with multiple questions.' };
49
114
  }
@@ -52,6 +117,12 @@ function pickLane(message, opts = {}) {
52
117
  || (WEIGHING_WORD_RE.test(text) && commaCount >= WEIGHING_FACTOR_MIN_COMMAS)) {
53
118
  return { lane: 'max', reason: 'max fits a decision weighing several factors.' };
54
119
  }
120
+ if (INCIDENT_RE.test(text)) {
121
+ return { lane: 'max', reason: 'max fits a live incident, stakes beat size.' };
122
+ }
123
+ if (LEGAL_DRAFT_RE.test(text) && /\b(?:draft|write|cover|review)\b/i.test(text)) {
124
+ return { lane: 'max', reason: 'max fits drafting legal terms.' };
125
+ }
55
126
  if (HEAVY_REASONING_RE.test(text)) {
56
127
  return { lane: 'max', reason: 'max fits this reasoning-heavy request.' };
57
128
  }
@@ -64,7 +135,12 @@ function pickLane(message, opts = {}) {
64
135
  if (ERROR_EXPLAIN_RE.test(text) && !hasCodeContext) {
65
136
  return { lane: 'fast', reason: 'fast fits a plain error explanation.' };
66
137
  }
67
- if (TRANSFORM_START_RE.test(text) && text.length <= shortLookupMaxChars) {
138
+ if (SPANISH_LOOKUP_RE.test(text) && text.length <= shortLookupMaxChars
139
+ && !JUDGMENT_WORD_RE.test(text) && !TROUBLE_RE.test(text)) {
140
+ return { lane: 'fast', reason: 'fast fits this short lookup.' };
141
+ }
142
+ if (TRANSFORM_START_RE.test(text) && text.length <= shortLookupMaxChars
143
+ && !OWN_CONTEXT_RE.test(text)) {
68
144
  return { lane: 'fast', reason: 'fast fits this small transform.' };
69
145
  }
70
146
  if (GREETING_RE.test(text) && text.length <= GREETING_MAX_CHARS) {
@@ -73,16 +149,24 @@ function pickLane(message, opts = {}) {
73
149
  if (text.length > 0 && text.length <= TINY_MESSAGE_MAX_CHARS && !CODE_EDIT_RE.test(text)) {
74
150
  return { lane: 'fast', reason: 'fast fits this tiny message.' };
75
151
  }
152
+ if (text.length <= HOWTO_LOOKUP_MAX_CHARS
153
+ && HOWTO_LOOKUP_RE.test(text)
154
+ && !JUDGMENT_WORD_RE.test(text)
155
+ && !TROUBLE_RE.test(text)) {
156
+ return { lane: 'fast', reason: 'fast fits this quick how-to.' };
157
+ }
76
158
  if (text.length <= YESNO_LOOKUP_MAX_CHARS
77
159
  && YESNO_LOOKUP_RE.test(text)
78
160
  && !JUDGMENT_WORD_RE.test(text)
79
- && !TROUBLE_RE.test(text)) {
161
+ && !TROUBLE_RE.test(text)
162
+ && !DEEP_TOPIC_RE.test(text)) {
80
163
  return { lane: 'fast', reason: 'fast fits this yes or no lookup.' };
81
164
  }
82
165
  if (text.length <= shortLookupMaxChars
83
166
  && LOOKUP_START_RE.test(text)
84
167
  && !JUDGMENT_WORD_RE.test(text)
85
- && !TROUBLE_RE.test(text)) {
168
+ && !TROUBLE_RE.test(text)
169
+ && !DEEP_TOPIC_RE.test(text)) {
86
170
  return { lane: 'fast', reason: 'fast fits this short factual lookup.' };
87
171
  }
88
172
  return { lane: 'pro', reason: 'pro fits this general request.' };
@@ -91,6 +175,6 @@ function pickLane(message, opts = {}) {
91
175
  module.exports = {
92
176
  LONG_INPUT_MIN_CHARS,
93
177
  SHORT_LOOKUP_MAX_CHARS,
94
- TINY_MESSAGE_MAX_CHARS,
178
+ loadOverrides,
95
179
  pickLane,
96
180
  };
@@ -140,8 +140,6 @@ async function withBenchContext(options, fn) {
140
140
  }
141
141
 
142
142
  module.exports = {
143
- PROFILE_ENV_KEYS,
144
143
  createBenchContext,
145
- hermeticEnv,
146
144
  withBenchContext,
147
145
  };
@@ -134,7 +134,6 @@ function renderBenchReportText(report) {
134
134
 
135
135
  module.exports = {
136
136
  DEFAULT_PACK,
137
- REPORT_SCHEMA,
138
137
  buildBenchReport,
139
138
  renderBenchReportText,
140
139
  };
@@ -492,21 +492,10 @@ function packMetadata(options = {}) {
492
492
  module.exports = {
493
493
  BenchInfraError,
494
494
  DEFAULT_PACK,
495
- appendResultRecord,
496
- benchmarksRoot,
497
- defaultTasksDir,
498
- discoverPacks,
499
- exitCodeForRecord,
500
495
  findPython,
501
- isInfraFailure,
502
- loadTaskSpecs,
503
496
  packMetadata,
504
497
  readResultRecords,
505
498
  runBench,
506
- runTaskSpec,
507
- runTaskSpecs,
508
- selectTaskSpecs,
509
499
  summarizeTasks,
510
500
  taskMetadata,
511
- writeBaselineRecord,
512
501
  };
@@ -331,17 +331,11 @@ function renderBriefReview(review = {}, { lessons = false } = {}) {
331
331
  }
332
332
 
333
333
  module.exports = {
334
- LEDGER_FILE,
335
- OUTCOMES,
336
334
  appendBriefRecord,
337
- authorFromWorktree,
338
- briefSignals,
339
335
  buildBriefReview,
340
- buildLessons,
341
336
  ledgerPath,
342
337
  latestBriefs,
343
338
  mirrorBriefRecord,
344
- normalizeRecord,
345
339
  readBriefLedger,
346
340
  renderBriefReview,
347
341
  stampBriefOutcome,
@@ -0,0 +1,382 @@
1
+ 'use strict';
2
+
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+
6
+ const SIMULATE_USAGE = 'Usage: atris business simulate "<idea>" [--roles a,b,c,d] [--name <business name>] [--dry-run]';
7
+ const DEFAULT_ROLE_NAMES = Object.freeze(['maker', 'voice', 'builder', 'ops']);
8
+
9
+ const NAME_STOP_WORDS = new Set([
10
+ 'a', 'an', 'and', 'at', 'build', 'by', 'create', 'for', 'from', 'in',
11
+ 'launch', 'make', 'my', 'of', 'on', 'our', 'sell', 'start', 'the', 'to',
12
+ 'with',
13
+ ]);
14
+
15
+ const ROLE_TEMPLATES = Object.freeze([
16
+ {
17
+ key: 'maker',
18
+ role: 'Product and design',
19
+ description: 'Owns product direction, design, and concrete product artifacts',
20
+ mission: (idea) => `Turn "${idea}" into a concrete product direction with usable design artifacts.`,
21
+ goals: (idea, roleNames) => [
22
+ `Define the smallest product version of "${idea}" that a customer could understand and use`,
23
+ 'Create the core product or design artifacts needed by the rest of the team',
24
+ `Hand the ${roleNames[1]} and ${roleNames[2]} roles clear inputs, constraints, and final source files`,
25
+ ],
26
+ doneWhen: '`product/` holds a clear product brief, final source artifacts, and handoff notes the other roles can use.',
27
+ rules: [
28
+ 'Make concrete artifacts, not moodboards or vague strategy.',
29
+ 'Do not claim something is ready until the source files and proof are in the workspace.',
30
+ ],
31
+ },
32
+ {
33
+ key: 'voice',
34
+ role: 'Brand and copy',
35
+ description: 'Owns positioning, brand voice, product copy, and launch drafts',
36
+ mission: (idea) => `Make "${idea}" understandable in five seconds and memorable the next day.`,
37
+ goals: (_idea, roleNames) => [
38
+ 'Write a one-page brand brief with audience, promise, voice, and a short story',
39
+ `Write the customer-facing product and surface copy the ${roleNames[2]} role needs`,
40
+ 'Draft a small launch package without posting or contacting anyone',
41
+ ],
42
+ doneWhen: '`brand/` holds the brand brief, final product copy, and clearly marked launch drafts.',
43
+ rules: [
44
+ 'Use plain, specific language and remove generic hype.',
45
+ 'Draft only. Never publish, message customers, or make external claims without approval.',
46
+ ],
47
+ },
48
+ {
49
+ key: 'builder',
50
+ role: 'Surface and storefront',
51
+ description: 'Owns the customer-facing surface and the path through it',
52
+ mission: (idea) => `Build a working customer-facing surface for "${idea}" that makes the next action obvious.`,
53
+ goals: () => [
54
+ 'Build the smallest complete surface that presents the real product and copy',
55
+ 'Connect every primary action to a safe simulated or test-mode path',
56
+ 'Verify the full path at phone and desktop sizes and record the proof',
57
+ ],
58
+ doneWhen: 'A customer can open the surface, understand the offer, and complete the simulated path without a broken step.',
59
+ rules: [
60
+ 'Use only existing approved accounts and deployment rails.',
61
+ 'Keep payments, publishing, and customer contact simulated until a human approves them.',
62
+ ],
63
+ },
64
+ {
65
+ key: 'ops',
66
+ role: 'Numbers and fulfillment',
67
+ description: 'Owns economics, operating choices, fulfillment, and the human handoff',
68
+ mission: (idea) => `Make the numbers and operating path for "${idea}" honest enough to act on tomorrow morning.`,
69
+ goals: () => [
70
+ 'Build simple unit economics with sourced assumptions and the key margin or viability test',
71
+ 'Compare practical fulfillment or delivery options and recommend one with reasons',
72
+ 'Write the exact checklist of account, payment, legal, or external steps left for a human',
73
+ ],
74
+ doneWhen: '`ops/` holds the economics, the operating recommendation, and a human-steps checklist.',
75
+ rules: [
76
+ 'Use sourced numbers and label every assumption.',
77
+ 'Never create accounts, spend money, enter payment details, or hide a human-only step.',
78
+ ],
79
+ },
80
+ ]);
81
+
82
+ function compactText(value) {
83
+ return String(value || '').replace(/\s+/g, ' ').trim();
84
+ }
85
+
86
+ function slugify(value) {
87
+ return compactText(value)
88
+ .toLowerCase()
89
+ .replace(/[^a-z0-9\s-]/g, '')
90
+ .replace(/\s+/g, '-')
91
+ .replace(/-+/g, '-')
92
+ .replace(/^-+|-+$/g, '');
93
+ }
94
+
95
+ function titleCaseWord(word) {
96
+ return String(word || '')
97
+ .toLowerCase()
98
+ .split('-')
99
+ .map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : '')
100
+ .join('-');
101
+ }
102
+
103
+ function titleCase(value) {
104
+ return compactText(value)
105
+ .split(/[\s_-]+/)
106
+ .filter(Boolean)
107
+ .map(titleCaseWord)
108
+ .join(' ');
109
+ }
110
+
111
+ function deriveBusinessName(idea, fallback = 'Business Simulation') {
112
+ const words = compactText(idea).match(/[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)*/g) || [];
113
+ const meaningful = words.filter((word) => !NAME_STOP_WORDS.has(word.toLowerCase()));
114
+ if (meaningful.length === 0) return fallback;
115
+ return meaningful.slice(0, 3).map(titleCaseWord).join(' ');
116
+ }
117
+
118
+ function normalizeRoleNames(value) {
119
+ const rawNames = Array.isArray(value)
120
+ ? value
121
+ : String(value || '').split(',');
122
+ const names = rawNames.map((name) => slugify(name)).filter(Boolean);
123
+
124
+ if (names.length !== ROLE_TEMPLATES.length) {
125
+ throw new Error(`--roles requires exactly ${ROLE_TEMPLATES.length} comma-separated role names.`);
126
+ }
127
+ if (new Set(names).size !== names.length) {
128
+ throw new Error('--roles must contain four distinct role names.');
129
+ }
130
+ return names;
131
+ }
132
+
133
+ function parseRoleNames(value) {
134
+ if (value === undefined || value === null || compactText(value) === '') {
135
+ return [...DEFAULT_ROLE_NAMES];
136
+ }
137
+ return normalizeRoleNames(value);
138
+ }
139
+
140
+ function buildRoleSet(idea, roleNames = DEFAULT_ROLE_NAMES) {
141
+ const cleanIdea = compactText(idea);
142
+ const names = normalizeRoleNames(roleNames);
143
+
144
+ return ROLE_TEMPLATES.map((template, index) => ({
145
+ key: template.key,
146
+ name: names[index],
147
+ title: titleCase(names[index]),
148
+ role: template.role,
149
+ description: template.description,
150
+ mission: template.mission(cleanIdea),
151
+ goals: template.goals(cleanIdea, names),
152
+ doneWhen: template.doneWhen,
153
+ rules: [...template.rules],
154
+ }));
155
+ }
156
+
157
+ function renderMemberMarkdown(role) {
158
+ return [
159
+ '---',
160
+ `name: ${role.name}`,
161
+ `role: ${role.title}`,
162
+ `description: ${role.description}`,
163
+ 'version: 1.0.0',
164
+ '',
165
+ 'skills: []',
166
+ '',
167
+ 'permissions:',
168
+ ' can-read: true',
169
+ ' can-execute: true',
170
+ ' can-approve: false',
171
+ ' can-accept-task: false',
172
+ ' approval-required: []',
173
+ '',
174
+ 'tools: []',
175
+ '---',
176
+ '',
177
+ `# ${role.title}`,
178
+ '',
179
+ '## Mission',
180
+ role.mission,
181
+ '',
182
+ '## Goals',
183
+ ...role.goals.map((goal) => `- ${goal}`),
184
+ '',
185
+ '## Done when',
186
+ role.doneWhen,
187
+ '',
188
+ '## Rules',
189
+ ...role.rules.map((rule) => `- ${rule}`),
190
+ '',
191
+ ].join('\n');
192
+ }
193
+
194
+ function renderMissionMarkdown(role) {
195
+ return [
196
+ `# ${role.title} Mission`,
197
+ '',
198
+ '## North Star',
199
+ role.mission,
200
+ '',
201
+ '## Loop',
202
+ '',
203
+ '1. Read `atris/ENDGAME.md` and the latest tick log.',
204
+ '2. Take the smallest unfinished item owned by this role.',
205
+ '3. Write the artifact and proof into the workspace.',
206
+ '4. Append the result, blocker, and next handoff to the tick log.',
207
+ '',
208
+ '## Stop condition',
209
+ '',
210
+ `Stop when ${role.doneWhen.charAt(0).toLowerCase()}${role.doneWhen.slice(1)}`,
211
+ '',
212
+ ].join('\n');
213
+ }
214
+
215
+ function formatCreationTime(createdAt) {
216
+ const date = createdAt instanceof Date ? createdAt : new Date(createdAt);
217
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid simulation creation time: ${createdAt}`);
218
+ return date.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
219
+ }
220
+
221
+ function renderEndgame({ businessName, idea, roles, createdAt }) {
222
+ const definition = roles.map((role, index) =>
223
+ `${index + 1}. [ ] ${role.title} (${role.name}): ${role.doneWhen}`
224
+ );
225
+ definition.push(`${roles.length + 1}. [ ] Integration: all four role outputs work together as one coherent, verified customer path.`);
226
+
227
+ return [
228
+ `# ${businessName} - Endgame`,
229
+ '',
230
+ `One night, one goal: make "${compactText(idea)}" launch-ready as a coherent business simulation. Everything done except the steps only a human can do.`,
231
+ '',
232
+ '## Definition of done',
233
+ ...definition,
234
+ '',
235
+ '## Blocked on human (by design)',
236
+ 'Real accounts, money, legal acceptance, publishing, customer contact, and irreversible external actions stay on the operator checklist.',
237
+ '',
238
+ '## Tick log',
239
+ `- ${formatCreationTime(createdAt)} - business created, team hired, missions written. Ready for the first loop.`,
240
+ '',
241
+ ].join('\n');
242
+ }
243
+
244
+ function parseSimulationArgs(args = []) {
245
+ const positional = [];
246
+ let explicitName = '';
247
+ let rolesValue;
248
+ let dryRun = false;
249
+
250
+ for (let index = 0; index < args.length; index++) {
251
+ const arg = args[index];
252
+ const next = args[index + 1];
253
+
254
+ if (arg === '--dry-run' || arg === '-n') {
255
+ dryRun = true;
256
+ } else if (arg === '--name') {
257
+ if (!next || next.startsWith('-')) throw new Error(`Missing value for --name.\n${SIMULATE_USAGE}`);
258
+ explicitName = next;
259
+ index++;
260
+ } else if (arg.startsWith('--name=')) {
261
+ explicitName = arg.slice('--name='.length);
262
+ } else if (arg === '--roles') {
263
+ if (!next || next.startsWith('-')) throw new Error(`Missing value for --roles.\n${SIMULATE_USAGE}`);
264
+ rolesValue = next;
265
+ index++;
266
+ } else if (arg.startsWith('--roles=')) {
267
+ rolesValue = arg.slice('--roles='.length);
268
+ } else if (arg.startsWith('-')) {
269
+ throw new Error(`Unknown option: ${arg}\n${SIMULATE_USAGE}`);
270
+ } else {
271
+ positional.push(arg);
272
+ }
273
+ }
274
+
275
+ const idea = compactText(positional.join(' '));
276
+ if (!idea) throw new Error(SIMULATE_USAGE);
277
+
278
+ const businessName = compactText(explicitName) || deriveBusinessName(idea);
279
+ const businessSlug = slugify(businessName);
280
+ if (!businessSlug) throw new Error(`Business name must contain a letter or number.\n${SIMULATE_USAGE}`);
281
+
282
+ return {
283
+ idea,
284
+ businessName,
285
+ businessSlug,
286
+ roleNames: parseRoleNames(rolesValue),
287
+ dryRun,
288
+ };
289
+ }
290
+
291
+ function shellQuote(value) {
292
+ return JSON.stringify(String(value));
293
+ }
294
+
295
+ function buildSimulationPlan({
296
+ idea,
297
+ businessName,
298
+ businessSlug,
299
+ roleNames = DEFAULT_ROLE_NAMES,
300
+ createdAt,
301
+ homeDir = os.homedir(),
302
+ workspacePath,
303
+ }) {
304
+ const cleanIdea = compactText(idea);
305
+ const name = compactText(businessName) || deriveBusinessName(cleanIdea);
306
+ const slug = slugify(businessSlug || name) || 'business-simulation';
307
+ const roles = buildRoleSet(cleanIdea, roleNames);
308
+ const created = createdAt || new Date().toISOString();
309
+ const root = workspacePath || path.join(homeDir, 'arena', 'atris-business', slug);
310
+
311
+ const files = [];
312
+ for (const role of roles) {
313
+ files.push({
314
+ path: `atris/team/${role.name}/MEMBER.md`,
315
+ content: renderMemberMarkdown(role),
316
+ kind: 'member',
317
+ role,
318
+ });
319
+ files.push({
320
+ path: `atris/team/${role.name}/MISSION.md`,
321
+ content: renderMissionMarkdown(role),
322
+ kind: 'mission',
323
+ role,
324
+ });
325
+ }
326
+ files.push({
327
+ path: 'atris/ENDGAME.md',
328
+ content: renderEndgame({ businessName: name, idea: cleanIdea, roles, createdAt: created }),
329
+ kind: 'endgame',
330
+ });
331
+
332
+ return {
333
+ idea: cleanIdea,
334
+ businessName: name,
335
+ businessSlug: slug,
336
+ workspacePath: root,
337
+ createdAt: created,
338
+ roles,
339
+ files,
340
+ commands: [
341
+ `atris business init ${shellQuote(name)}`,
342
+ ...roles.flatMap((role) => [
343
+ `atris member create ${role.name} --role=${shellQuote(role.title)} --description=${shellQuote(role.description)}`,
344
+ `atris member push ${role.name}`,
345
+ ]),
346
+ ],
347
+ };
348
+ }
349
+
350
+ function renderDryRun(plan) {
351
+ const lines = [
352
+ 'dry run: no cloud calls made and no files written.',
353
+ '',
354
+ `business: ${plan.businessName} (${plan.businessSlug})`,
355
+ `idea: ${plan.idea}`,
356
+ `workspace: ${plan.workspacePath}`,
357
+ '',
358
+ 'would run:',
359
+ ...plan.commands.map((command) => ` ${command}`),
360
+ '',
361
+ `roster: ${plan.roles.map((role) => `${role.name} (${role.role})`).join(', ')}`,
362
+ ];
363
+
364
+ for (const file of plan.files) {
365
+ lines.push('', `--- ${file.path} ---`, file.content.trimEnd());
366
+ }
367
+ lines.push('');
368
+ return lines.join('\n');
369
+ }
370
+
371
+ module.exports = {
372
+ SIMULATE_USAGE,
373
+ buildRoleSet,
374
+ buildSimulationPlan,
375
+ deriveBusinessName,
376
+ parseRoleNames,
377
+ parseSimulationArgs,
378
+ renderDryRun,
379
+ renderEndgame,
380
+ renderMemberMarkdown,
381
+ slugify,
382
+ };
@@ -369,9 +369,4 @@ module.exports = {
369
369
  SCHEMA,
370
370
  scanChatLogs,
371
371
  writeLatestScan,
372
- readLatestScan,
373
- ensureChatScan,
374
- topChatFindings,
375
- cursorTranscriptRoots,
376
- axLogDirs,
377
372
  };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('node:child_process');
4
+
5
+ function runGit(args, cwd = process.cwd()) {
6
+ return spawnSync('git', args, {
7
+ cwd,
8
+ encoding: 'utf8',
9
+ timeout: 30000,
10
+ });
11
+ }
12
+
13
+ function checkoutBehind(cwd = process.cwd()) {
14
+ const countResult = runGit(['rev-list', '--count', 'HEAD..@{upstream}'], cwd);
15
+ if (countResult.status !== 0) return null;
16
+
17
+ const count = Number.parseInt(countResult.stdout.trim(), 10);
18
+ if (!Number.isInteger(count) || count < 1) return null;
19
+
20
+ const upstreamResult = runGit(
21
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
22
+ cwd
23
+ );
24
+ const upstream = upstreamResult.status === 0
25
+ ? upstreamResult.stdout.trim()
26
+ : 'origin';
27
+ const remote = upstream.includes('/') ? upstream.split('/')[0] : upstream;
28
+
29
+ return { count, upstream, remote };
30
+ }
31
+
32
+ function checkoutBehindMessage(cwd = process.cwd()) {
33
+ const behind = checkoutBehind(cwd);
34
+ if (!behind) return null;
35
+ const unit = behind.count === 1 ? 'commit' : 'commits';
36
+ return `this checkout is ${behind.count} ${unit} behind ${behind.remote}; run atris sync-checkout`;
37
+ }
38
+
39
+ function trackedTreeIsClean(cwd = process.cwd()) {
40
+ const unstaged = runGit(['diff', '--quiet', '--'], cwd);
41
+ if (unstaged.status !== 0) return false;
42
+
43
+ const staged = runGit(['diff', '--cached', '--quiet', '--'], cwd);
44
+ return staged.status === 0;
45
+ }
46
+
47
+ module.exports = {
48
+ checkoutBehind,
49
+ checkoutBehindMessage,
50
+ runGit,
51
+ trackedTreeIsClean,
52
+ };