atris 3.37.1 → 3.40.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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
@@ -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,14 +13,14 @@ 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;
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;
20
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.
@@ -25,6 +29,12 @@ const YESNO_LOOKUP_RE = /^(?:does|is|are|can|do)\b/i;
25
29
  const HOWTO_LOOKUP_RE = /^how (?:do|does|did) (?:i|we|you)\b/i;
26
30
  const HOWTO_LOOKUP_MAX_CHARS = 40;
27
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;
28
38
  // Troubleshooting language means advice, not recall: keep it out of fast.
29
39
  const TROUBLE_RE = /\bfail(?:s|ed|ing)?\b|\bcrash(?:es|ed|ing)?\b|\bbroken\b|\bnot working\b/i;
30
40
  const WEIGHING_WORD_RE = /\b(?:consider|weigh|rank|estimate|think (?:about|through))\b/i;
@@ -42,6 +52,38 @@ function threshold(value, fallback) {
42
52
  return Number.isFinite(value) && value >= 0 ? value : fallback;
43
53
  }
44
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
+
45
87
  function pickLane(message, opts = {}) {
46
88
  const text = String(message || '').trim();
47
89
  const shortLookupMaxChars = threshold(opts.shortLookupMaxChars, SHORT_LOOKUP_MAX_CHARS);
@@ -50,6 +92,17 @@ function pickLane(message, opts = {}) {
50
92
  const commaCount = (text.match(/,/g) || []).length;
51
93
  const hasCodeContext = CODE_CONTEXT_RE.test(text);
52
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
+
53
106
  if (text.length > longInputMinChars) {
54
107
  return { lane: 'max', reason: 'max fits this long input.' };
55
108
  }
@@ -64,6 +117,12 @@ function pickLane(message, opts = {}) {
64
117
  || (WEIGHING_WORD_RE.test(text) && commaCount >= WEIGHING_FACTOR_MIN_COMMAS)) {
65
118
  return { lane: 'max', reason: 'max fits a decision weighing several factors.' };
66
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
+ }
67
126
  if (HEAVY_REASONING_RE.test(text)) {
68
127
  return { lane: 'max', reason: 'max fits this reasoning-heavy request.' };
69
128
  }
@@ -99,13 +158,15 @@ function pickLane(message, opts = {}) {
99
158
  if (text.length <= YESNO_LOOKUP_MAX_CHARS
100
159
  && YESNO_LOOKUP_RE.test(text)
101
160
  && !JUDGMENT_WORD_RE.test(text)
102
- && !TROUBLE_RE.test(text)) {
161
+ && !TROUBLE_RE.test(text)
162
+ && !DEEP_TOPIC_RE.test(text)) {
103
163
  return { lane: 'fast', reason: 'fast fits this yes or no lookup.' };
104
164
  }
105
165
  if (text.length <= shortLookupMaxChars
106
166
  && LOOKUP_START_RE.test(text)
107
167
  && !JUDGMENT_WORD_RE.test(text)
108
- && !TROUBLE_RE.test(text)) {
168
+ && !TROUBLE_RE.test(text)
169
+ && !DEEP_TOPIC_RE.test(text)) {
109
170
  return { lane: 'fast', reason: 'fast fits this short factual lookup.' };
110
171
  }
111
172
  return { lane: 'pro', reason: 'pro fits this general request.' };
@@ -114,6 +175,6 @@ function pickLane(message, opts = {}) {
114
175
  module.exports = {
115
176
  LONG_INPUT_MIN_CHARS,
116
177
  SHORT_LOOKUP_MAX_CHARS,
117
- TINY_MESSAGE_MAX_CHARS,
178
+ loadOverrides,
118
179
  pickLane,
119
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
  };
@@ -219,6 +219,22 @@ async function runSetup(spec, ctx) {
219
219
  await setup(ctx);
220
220
  }
221
221
 
222
+ // The one live prompt gate (lesson: benchmark-prompt-paths): receipt tests run
223
+ // the solution/null engines, which never read the prompt, so a malformed
224
+ // prompt.md sails through green dry-runs and only a paid real-engine run finds
225
+ // it. Every prompt an engine receives flows through here, and the tests assert
226
+ // real task prompts against the same function.
227
+ function readTaskPrompt(spec) {
228
+ const text = fs.readFileSync(spec.promptPath, 'utf8');
229
+ if (!text.trim() || text.trim().length < 20) {
230
+ throw new BenchInfraError(`${spec.id}: prompt.md is empty or too short to brief an engine`);
231
+ }
232
+ if (/\{\{[^}]*\}\}|<%|\uFFFD|\u0000/.test(text)) {
233
+ throw new BenchInfraError(`${spec.id}: prompt.md carries unresolved template or encoding artifacts`);
234
+ }
235
+ return text;
236
+ }
237
+
222
238
  function formatEngineFailure(engineName, result) {
223
239
  const stdout = String(result && result.stdout ? result.stdout : '').trim();
224
240
  const stderr = String(result && result.stderr ? result.stderr : '').trim();
@@ -256,7 +272,7 @@ async function runAgentTaskSpec(spec, options = {}) {
256
272
  }
257
273
 
258
274
  await runSetup(spec, ctx);
259
- const promptText = fs.readFileSync(spec.promptPath, 'utf8');
275
+ const promptText = readTaskPrompt(spec);
260
276
  const engineResult = await withTimeout(Promise.resolve(engine.run(promptText, ctx.workspace, timeoutMs)), spec, timeoutMs);
261
277
  if (!engineResult || engineResult.status !== 0) {
262
278
  throw new Error(formatEngineFailure(options.engine, engineResult));
@@ -492,21 +508,12 @@ function packMetadata(options = {}) {
492
508
  module.exports = {
493
509
  BenchInfraError,
494
510
  DEFAULT_PACK,
495
- appendResultRecord,
496
- benchmarksRoot,
497
- defaultTasksDir,
498
- discoverPacks,
499
- exitCodeForRecord,
500
511
  findPython,
501
- isInfraFailure,
502
512
  loadTaskSpecs,
503
513
  packMetadata,
504
514
  readResultRecords,
515
+ readTaskPrompt,
505
516
  runBench,
506
- runTaskSpec,
507
- runTaskSpecs,
508
- selectTaskSpecs,
509
517
  summarizeTasks,
510
518
  taskMetadata,
511
- writeBaselineRecord,
512
519
  };
@@ -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
+ };