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.
- package/AGENTS.md +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/computer.js
CHANGED
|
@@ -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(
|
|
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
|
+
};
|