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
@@ -2,6 +2,7 @@
2
2
  * Atris Computer — interact with your EC2 AI Computer
3
3
  *
4
4
  * atris computer — Open SMART mode (cloud in business workspace, local elsewhere)
5
+ * atris computer setup — Connect coding accounts to a computer
5
6
  * atris computer --cloud — Open CLOUD workspace mode
6
7
  * atris computer create <name> — Create and wake a business computer
7
8
  * atris computer wake — Start the computer
@@ -21,9 +22,18 @@ const os = require('os');
21
22
  const path = require('path');
22
23
  const readline = require('readline');
23
24
  const { spawnSync } = require('child_process');
24
- const { loadCredentials, decodeJwtClaims } = require('../utils/auth');
25
+ const { loadCredentials, decodeJwtClaims, promptUser } = require('../utils/auth');
25
26
  const { apiRequestJson, getApiBaseUrl, getAppBaseUrl } = require('../utils/api');
26
27
  const { loadBusinesses, saveBusinesses } = require('./business');
28
+ const {
29
+ engineLoginSeatError,
30
+ formatEngineSeats,
31
+ listEngineSeats,
32
+ normalizeEngineLoginSeat,
33
+ readyCheckEngineLogin,
34
+ runEngineDeviceLoginCommand,
35
+ validEngineLoginSeat,
36
+ } = require('./engine');
27
37
  const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
28
38
  const { streamSession } = require('./serve');
29
39
  const { buildRemoteAtrisBootstrapCommand } = require('../lib/runtime-bootstrap');
@@ -41,6 +51,132 @@ function sleep(ms) {
41
51
  return new Promise((resolve) => setTimeout(resolve, ms));
42
52
  }
43
53
 
54
+ async function askComputerSetup(question, deps = {}) {
55
+ const prompt = deps.prompt || promptUser;
56
+ return String(await prompt(question) || '').trim();
57
+ }
58
+
59
+ function setupComputerChoices(data) {
60
+ const businesses = Array.isArray(data)
61
+ ? data
62
+ : (Array.isArray(data?.businesses) ? data.businesses : []);
63
+ return businesses
64
+ .filter((business) => business && business.id)
65
+ .map((business) => {
66
+ const owner = business.name || business.slug || 'business';
67
+ const computer = business.computer_name || business.workspace_name || '';
68
+ return {
69
+ label: computer ? `${owner}: ${computer}` : `${owner} computer`,
70
+ target: { type: 'business', id: business.id },
71
+ };
72
+ });
73
+ }
74
+
75
+ async function chooseSetupComputer(computers, deps = {}) {
76
+ const choices = computers.length
77
+ ? computers
78
+ : [{ label: 'your personal computer', target: { type: 'user' } }];
79
+ if (choices.length === 1) {
80
+ console.log(`using ${choices[0].label}.`);
81
+ return choices[0];
82
+ }
83
+
84
+ choices.forEach((computer, index) => console.log(`${index + 1}. ${computer.label}`));
85
+ const answer = await askComputerSetup('which computer? [1] ', deps);
86
+ const selected = Number.parseInt(answer || '1', 10);
87
+ return choices[selected >= 1 && selected <= choices.length ? selected - 1 : 0];
88
+ }
89
+
90
+ async function chooseSetupProviders(deps = {}) {
91
+ while (true) {
92
+ const answer = (await askComputerSetup(
93
+ 'which coding accounts do you want to connect? (codex / claude / both) [both] ',
94
+ deps
95
+ ) || 'both').toLowerCase();
96
+ if (answer === 'codex') return ['codex'];
97
+ if (answer === 'claude') return ['claude'];
98
+ if (answer === 'both') return ['codex', 'claude'];
99
+ console.error('choose codex, claude, or both.');
100
+ }
101
+ }
102
+
103
+ async function computerSetup(deps = {}) {
104
+ try {
105
+ const load = deps.loadCredentials || loadCredentials;
106
+ const credentials = await load();
107
+ if (!credentials?.token) {
108
+ console.error('run: atris login');
109
+ return 1;
110
+ }
111
+
112
+ const api = deps.apiRequestJson || apiRequestJson;
113
+ const computersResult = await api('/business/', {
114
+ method: 'GET',
115
+ token: credentials.token,
116
+ timeoutMs: 15000,
117
+ retries: 0,
118
+ });
119
+ if (!computersResult.ok) {
120
+ console.error('could not load your computers; please try again.');
121
+ return 1;
122
+ }
123
+
124
+ const computer = await chooseSetupComputer(setupComputerChoices(computersResult.data), deps);
125
+ const providers = await chooseSetupProviders(deps);
126
+ const deviceLogin = deps.runEngineDeviceLoginCommand || runEngineDeviceLoginCommand;
127
+ const readyCheck = deps.readyCheckEngineLogin || readyCheckEngineLogin;
128
+
129
+ for (const provider of providers) {
130
+ while (true) {
131
+ const providerHint = provider === 'claude'
132
+ ? ' you will get a code in the browser, paste it back here.'
133
+ : '';
134
+ const rawSeat = await askComputerSetup(
135
+ `connect a ${provider} account?${providerHint} give it a short name like personal or work (enter to finish) `,
136
+ deps
137
+ );
138
+ if (!rawSeat) break;
139
+ const seat = normalizeEngineLoginSeat(rawSeat);
140
+ if (!validEngineLoginSeat(seat)) {
141
+ console.error(engineLoginSeatError());
142
+ continue;
143
+ }
144
+
145
+ const code = await deviceLogin(provider, {
146
+ target: computer.target,
147
+ seat,
148
+ setup: true,
149
+ }, deps);
150
+ if (code !== 0) return 1;
151
+ let working = false;
152
+ try {
153
+ const checked = await readyCheck(provider, computer.target, deps);
154
+ working = Boolean(checked?.ok && checked.data?.status === 'ready');
155
+ } catch {}
156
+ const seatName = seat.toLowerCase();
157
+ console.log(working
158
+ ? `${seatName}: working`
159
+ : `${seatName}: linked, but the check failed - it may still work, try: atris engine seats`);
160
+ }
161
+ }
162
+
163
+ const listSeats = deps.listEngineSeats || listEngineSeats;
164
+ const seats = await listSeats(deps);
165
+ if (!seats.ok) {
166
+ console.error('could not check the linked accounts; please try again.');
167
+ return 1;
168
+ }
169
+
170
+ const now = typeof deps.now === 'function' ? deps.now() : Date.now();
171
+ console.log(formatEngineSeats(seats.data, now));
172
+ console.log('Done. Your computer can now work on these accounts.');
173
+ return 0;
174
+ } catch {
175
+ console.error('could not finish computer setup; please try again.');
176
+ return 1;
177
+ }
178
+ }
179
+
44
180
  const VALID_CLOUD_WORKERS = new Set(['claude', 'openai']);
45
181
  const LOCAL_BRIDGE_RECONNECT_MS = 2000;
46
182
  const VALID_COMPUTER_TYPES = new Set([
@@ -3974,12 +4110,18 @@ async function runRecruitingComputerShortcut(token, args, cloudOptions = {}) {
3974
4110
  }
3975
4111
  }
3976
4112
 
3977
- async function runComputer() {
3978
- const parsed = parseComputerOptions(process.argv.slice(3));
4113
+ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4114
+ const parsed = parseComputerOptions(argv);
3979
4115
  const args = parsed.positional;
3980
4116
  const cloudOptions = parsed.options;
3981
4117
  const sub = args[0];
3982
4118
 
4119
+ if (sub === 'setup') {
4120
+ const code = await computerSetup(deps);
4121
+ if (code !== 0) process.exitCode = code;
4122
+ return code;
4123
+ }
4124
+
3983
4125
  if (!sub) {
3984
4126
  if (cloudOptions.businessSlug || cloudOptions.workspaceId) {
3985
4127
  const token = getToken();
@@ -4085,6 +4227,7 @@ async function runComputer() {
4085
4227
  console.log('');
4086
4228
  console.log('Modes:');
4087
4229
  console.log(' (default) Choose CLOUD vs LOCAL when both are available');
4230
+ console.log(' setup Connect named Codex or Claude accounts to a computer');
4088
4231
  console.log(' card Show the local owner/computer card, no login required');
4089
4232
  console.log(' local Open LOCAL Atris mode; cloud brain edits this folder');
4090
4233
  console.log(' proof Run the local-edit + cloud-isolation + audit proof');
@@ -4124,6 +4267,7 @@ async function runComputer() {
4124
4267
  console.log('');
4125
4268
  console.log('Examples:');
4126
4269
  console.log(' atris computer');
4270
+ console.log(' atris computer setup');
4127
4271
  console.log(' atris computer card --write');
4128
4272
  console.log(' atris business init "My Lab" # first/default computer with Atris + operator');
4129
4273
  console.log(' atris computer create "Hiring Computer" --business my-lab --type recruiting');
@@ -4245,6 +4389,9 @@ async function runComputer() {
4245
4389
 
4246
4390
  module.exports = {
4247
4391
  runComputer,
4392
+ computerSetup,
4393
+ setupComputerChoices,
4394
+ formatEngineSeats,
4248
4395
  buildComputerCard,
4249
4396
  renderComputerCard,
4250
4397
  renderComputerCardMarkdown,
@@ -0,0 +1,365 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const {
7
+ DEFAULT_CLAUDE_RUNNER_MODEL,
8
+ RUNNER_PROFILE_DEFS,
9
+ } = require('../lib/runner-command');
10
+
11
+ const DREAMS_FILE = ['.atris', 'state', 'dreams.jsonl'];
12
+ const DAY_MS = 24 * 60 * 60 * 1000;
13
+ const DREAM_TIMEOUT_MS = 60 * 1000;
14
+ const MAX_CONTEXT_CHARS = 7000;
15
+ const MAX_SECTION_CHARS = 2400;
16
+
17
+ function dreamsPath(root) {
18
+ return path.join(root, ...DREAMS_FILE);
19
+ }
20
+
21
+ function safeRead(file) {
22
+ try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
23
+ }
24
+
25
+ function readJsonLines(file) {
26
+ const text = safeRead(file);
27
+ if (!text) return [];
28
+ return text.split(/\r?\n/)
29
+ .map((line) => line.trim())
30
+ .filter(Boolean)
31
+ .map((line) => {
32
+ try { return JSON.parse(line); } catch { return null; }
33
+ })
34
+ .filter(Boolean);
35
+ }
36
+
37
+ function appendDreamRows(root, rows) {
38
+ if (!rows.length) return [];
39
+ const file = dreamsPath(root);
40
+ fs.mkdirSync(path.dirname(file), { recursive: true });
41
+ fs.appendFileSync(file, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
42
+ return rows;
43
+ }
44
+
45
+ function rowTimestamp(row) {
46
+ const keys = ['ts', 'at', 'created_at', 'updated_at', 'completed_at', 'finished_at', 'generated_at'];
47
+ for (const key of keys) {
48
+ const parsed = Date.parse(String(row && row[key] || ''));
49
+ if (Number.isFinite(parsed)) return parsed;
50
+ }
51
+ return NaN;
52
+ }
53
+
54
+ function isRecentTime(ms, nowMs, windowMs = DAY_MS) {
55
+ return Number.isFinite(ms) && ms >= nowMs - windowMs && ms <= nowMs + 5 * 60 * 1000;
56
+ }
57
+
58
+ function latestJournalFiles(root, limit = 2) {
59
+ const logsDir = path.join(root, 'atris', 'logs');
60
+ let years;
61
+ try { years = fs.readdirSync(logsDir).filter((name) => /^\d{4}$/.test(name)).sort(); } catch { return []; }
62
+ const files = [];
63
+ for (const year of years) {
64
+ const dir = path.join(logsDir, year);
65
+ let names;
66
+ try { names = fs.readdirSync(dir).filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name)); } catch { names = []; }
67
+ for (const name of names) files.push(path.join(dir, name));
68
+ }
69
+ return files.sort().slice(-limit);
70
+ }
71
+
72
+ function clipTail(text, maxChars) {
73
+ const clean = String(text || '').trim();
74
+ if (clean.length <= maxChars) return clean;
75
+ return `[cut]\n${clean.slice(clean.length - maxChars + 6)}`;
76
+ }
77
+
78
+ function addSection(parts, budget, label, text, maxChars = MAX_SECTION_CHARS) {
79
+ const clean = String(text || '').trim();
80
+ if (!clean || budget.left <= 0) return;
81
+ const header = `\n[${label}]\n`;
82
+ const room = Math.min(maxChars, Math.max(0, budget.left - header.length));
83
+ if (room <= 0) return;
84
+ const body = clipTail(clean, room);
85
+ parts.push(`${header}${body}`);
86
+ budget.left -= header.length + body.length;
87
+ }
88
+
89
+ function recentJsonlRows(root, relativePath, nowMs, limit = 20) {
90
+ const rows = readJsonLines(path.join(root, relativePath));
91
+ return rows
92
+ .filter((row) => isRecentTime(rowTimestamp(row), nowMs))
93
+ .sort((a, b) => rowTimestamp(a) - rowTimestamp(b))
94
+ .slice(-limit);
95
+ }
96
+
97
+ function relativeToRoot(root, file) {
98
+ return path.relative(root, file).replace(/\\/g, '/');
99
+ }
100
+
101
+ function recentMissionReceipts(root, nowMs, limit = 6) {
102
+ const runsDir = path.join(root, 'atris', 'runs');
103
+ let names;
104
+ try { names = fs.readdirSync(runsDir).filter((name) => /^mission-.*\.json$/.test(name)); } catch { return []; }
105
+ const receipts = [];
106
+ for (const name of names) {
107
+ const file = path.join(runsDir, name);
108
+ let stat;
109
+ try { stat = fs.statSync(file); } catch { continue; }
110
+ let parsed = null;
111
+ try { parsed = JSON.parse(safeRead(file)); } catch { parsed = null; }
112
+ const stamped = Number.isFinite(rowTimestamp(parsed)) ? rowTimestamp(parsed) : stat.mtimeMs;
113
+ if (!isRecentTime(stamped, nowMs)) continue;
114
+ receipts.push({ stamped, file: relativeToRoot(root, file), body: parsed || safeRead(file) });
115
+ }
116
+ return receipts.sort((a, b) => a.stamped - b.stamped).slice(-limit);
117
+ }
118
+
119
+ function collectDreamMaterial(root = process.cwd(), options = {}) {
120
+ const nowDate = options.now instanceof Date ? options.now : new Date(options.now || Date.now());
121
+ const nowMs = nowDate.getTime();
122
+ const parts = [];
123
+ const budget = { left: options.maxContextChars || MAX_CONTEXT_CHARS };
124
+
125
+ for (const file of latestJournalFiles(root, 2)) {
126
+ addSection(parts, budget, `journal ${path.basename(file, '.md')}`, safeRead(file));
127
+ }
128
+
129
+ const wishes = recentJsonlRows(root, path.join('.atris', 'state', 'wishes.jsonl'), nowMs);
130
+ if (wishes.length) addSection(parts, budget, 'wishes from the last day', wishes.map((row) => JSON.stringify(row)).join('\n'));
131
+
132
+ const missions = recentJsonlRows(root, path.join('.atris', 'state', 'missions.jsonl'), nowMs);
133
+ if (missions.length) addSection(parts, budget, 'missions from the last day', missions.map((row) => JSON.stringify(row)).join('\n'));
134
+
135
+ const receipts = recentMissionReceipts(root, nowMs);
136
+ if (receipts.length) {
137
+ addSection(parts, budget, 'mission receipts from the last day', receipts.map((receipt) => {
138
+ const body = typeof receipt.body === 'string' ? receipt.body : JSON.stringify(receipt.body);
139
+ return `${receipt.file}\n${body}`;
140
+ }).join('\n\n'));
141
+ }
142
+
143
+ return {
144
+ text: parts.join('\n').trim(),
145
+ sectionCount: parts.length,
146
+ now: nowDate.toISOString(),
147
+ };
148
+ }
149
+
150
+ function buildDreamPrompt(material) {
151
+ return [
152
+ 'You are atris dream.',
153
+ 'Read the day notes below.',
154
+ 'Return only JSON.',
155
+ 'Make 2 or 3 cards.',
156
+ 'Use this exact shape:',
157
+ '[{"title":"three to five plain words","why":"one short line","move":"one concrete next action"}]',
158
+ 'Use plain words a child gets.',
159
+ 'Do not use em dashes.',
160
+ '',
161
+ material,
162
+ ].join('\n');
163
+ }
164
+
165
+ function replaceLongDashes(value) {
166
+ return String(value || '').replace(/[\u2012-\u2015]/g, '-');
167
+ }
168
+
169
+ function oneLine(value, max = 180) {
170
+ return replaceLongDashes(value)
171
+ .replace(/\s+/g, ' ')
172
+ .trim()
173
+ .slice(0, max)
174
+ .trim();
175
+ }
176
+
177
+ function findJsonArrayText(output) {
178
+ const text = String(output || '').trim();
179
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
180
+ const target = fenced ? fenced[1] : text;
181
+ const start = target.indexOf('[');
182
+ if (start === -1) return '';
183
+ let depth = 0;
184
+ let inString = false;
185
+ let escaped = false;
186
+ for (let index = start; index < target.length; index += 1) {
187
+ const ch = target[index];
188
+ if (inString) {
189
+ if (escaped) escaped = false;
190
+ else if (ch === '\\') escaped = true;
191
+ else if (ch === '"') inString = false;
192
+ continue;
193
+ }
194
+ if (ch === '"') {
195
+ inString = true;
196
+ } else if (ch === '[') {
197
+ depth += 1;
198
+ } else if (ch === ']') {
199
+ depth -= 1;
200
+ if (depth === 0) return target.slice(start, index + 1);
201
+ }
202
+ }
203
+ return '';
204
+ }
205
+
206
+ function parseDreamCards(output) {
207
+ const jsonText = findJsonArrayText(output);
208
+ if (!jsonText) throw new Error('missing json');
209
+ let parsed;
210
+ try { parsed = JSON.parse(jsonText); } catch (error) { throw new Error(`bad json: ${error.message}`); }
211
+ if (!Array.isArray(parsed) || parsed.length < 2 || parsed.length > 3) {
212
+ throw new Error('expected 2 or 3 cards');
213
+ }
214
+ return parsed.map((card) => {
215
+ const title = oneLine(card && card.title, 80);
216
+ const why = oneLine(card && card.why);
217
+ const move = oneLine(card && card.move);
218
+ const wordCount = title ? title.split(/\s+/).length : 0;
219
+ if (!title || !why || !move || wordCount < 3 || wordCount > 5) {
220
+ throw new Error('bad card shape');
221
+ }
222
+ return { title, why, move };
223
+ });
224
+ }
225
+
226
+ function dreamModels() {
227
+ const models = [
228
+ RUNNER_PROFILE_DEFS.haiku && RUNNER_PROFILE_DEFS.haiku.model,
229
+ DEFAULT_CLAUDE_RUNNER_MODEL,
230
+ ].filter(Boolean);
231
+ return Array.from(new Set(models));
232
+ }
233
+
234
+ function defaultDreamRunner({ prompt, model, cwd, timeoutMs = DREAM_TIMEOUT_MS }) {
235
+ return new Promise((resolve) => {
236
+ const bin = (RUNNER_PROFILE_DEFS.haiku && RUNNER_PROFILE_DEFS.haiku.bin) || 'claude';
237
+ const proc = spawn(bin, ['-p', prompt, '--model', model], {
238
+ cwd,
239
+ stdio: ['ignore', 'pipe', 'pipe'],
240
+ });
241
+ let stdout = '';
242
+ let stderr = '';
243
+ let timedOut = false;
244
+ const timer = setTimeout(() => {
245
+ timedOut = true;
246
+ try { proc.kill('SIGTERM'); } catch {}
247
+ }, timeoutMs);
248
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
249
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
250
+ proc.on('close', (code) => {
251
+ clearTimeout(timer);
252
+ resolve({ ok: code === 0 && !timedOut, stdout, stderr, exitCode: code, timedOut });
253
+ });
254
+ proc.on('error', (error) => {
255
+ clearTimeout(timer);
256
+ resolve({ ok: false, error: error.message, stdout, stderr, timedOut });
257
+ });
258
+ });
259
+ }
260
+
261
+ function attemptFailureReason(result) {
262
+ if (result && result.garbled) return 'could not read dream cards';
263
+ if (result && result.timedOut) return 'model timed out';
264
+ if (result && result.error) return 'model could not start';
265
+ if (result && result.exitCode) return 'model stopped early';
266
+ return 'model did not answer';
267
+ }
268
+
269
+ function writeDreamCards(root, cards, stamp) {
270
+ const ts = stamp || new Date().toISOString();
271
+ return appendDreamRows(root, cards.map((card) => ({
272
+ ts,
273
+ title: card.title,
274
+ why: card.why,
275
+ move: card.move,
276
+ source: 'dream',
277
+ })));
278
+ }
279
+
280
+ function writeDreamNoop(root, reason, stamp) {
281
+ return appendDreamRows(root, [{
282
+ ts: stamp || new Date().toISOString(),
283
+ kind: 'dream_noop',
284
+ reason,
285
+ }])[0];
286
+ }
287
+
288
+ async function runDream(root = process.cwd(), options = {}) {
289
+ const stamp = options.stamp || new Date().toISOString();
290
+ try {
291
+ const material = collectDreamMaterial(root, options);
292
+ if (!material.text) return { ok: false, cards: [], reason: 'nothing to read' };
293
+
294
+ const prompt = buildDreamPrompt(material.text);
295
+ const runner = options.runner || defaultDreamRunner;
296
+ let lastFailure = null;
297
+ for (const model of dreamModels()) {
298
+ const result = await runner({
299
+ prompt,
300
+ model,
301
+ cwd: root,
302
+ timeoutMs: options.timeoutMs || DREAM_TIMEOUT_MS,
303
+ });
304
+ if (!result || result.ok === false || result.timedOut) {
305
+ lastFailure = result || { ok: false };
306
+ continue;
307
+ }
308
+ const output = result.stdout != null ? result.stdout : (result.result || result.output || '');
309
+ let cards;
310
+ try {
311
+ cards = parseDreamCards(output);
312
+ } catch {
313
+ lastFailure = { ok: false, garbled: true };
314
+ continue;
315
+ }
316
+ return { ok: true, cards: writeDreamCards(root, cards, stamp), reason: null };
317
+ }
318
+
319
+ const reason = attemptFailureReason(lastFailure);
320
+ writeDreamNoop(root, reason, stamp);
321
+ return { ok: false, cards: [], reason, noop: true };
322
+ } catch {
323
+ const reason = 'could not finish dream';
324
+ try { writeDreamNoop(root, reason, stamp); } catch {}
325
+ return { ok: false, cards: [], reason, noop: true };
326
+ }
327
+ }
328
+
329
+ function showHelp(log = console.log) {
330
+ log('');
331
+ log('Usage: atris dream');
332
+ log('');
333
+ log('Reads today and writes 2 or 3 cards for atris next.');
334
+ log('');
335
+ log('Run me nightly: atris dream');
336
+ log('launchd line: run atris dream at 4:00 every morning');
337
+ log('');
338
+ return 0;
339
+ }
340
+
341
+ async function dreamCommand(args = [], root = process.cwd(), options = {}) {
342
+ const log = options.log || console.log;
343
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') return showHelp(log);
344
+ const result = await runDream(root, options);
345
+ if (result.cards && result.cards.length) {
346
+ const noun = result.cards.length === 1 ? 'card' : 'cards';
347
+ log(`Dreamed ${result.cards.length} ${noun}.`);
348
+ } else {
349
+ log(`No dreams tonight: ${result.reason || 'nothing useful'}`);
350
+ }
351
+ log('Run me nightly: atris dream');
352
+ return 0;
353
+ }
354
+
355
+ module.exports = {
356
+ DREAM_TIMEOUT_MS,
357
+ dreamsPath,
358
+ collectDreamMaterial,
359
+ buildDreamPrompt,
360
+ parseDreamCards,
361
+ dreamModels,
362
+ defaultDreamRunner,
363
+ runDream,
364
+ dreamCommand,
365
+ };