atris 3.30.7 → 3.30.12

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.
@@ -0,0 +1,320 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn, spawnSync } = require('child_process');
4
+
5
+ function stripMissionQuotes(value) {
6
+ const text = String(value || '').trim();
7
+ if (text.length >= 2) {
8
+ const first = text[0];
9
+ const last = text[text.length - 1];
10
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
11
+ return text.slice(1, -1).trim();
12
+ }
13
+ }
14
+ return text;
15
+ }
16
+
17
+ function extractMissionFlag(text, flagName) {
18
+ const source = String(text || '');
19
+ const inline = new RegExp(`(^|\\s)${flagName}=([^\\s]+)(?=\\s|$)`).exec(source);
20
+ if (inline) {
21
+ return {
22
+ value: inline[2],
23
+ text: `${source.slice(0, inline.index)}${inline[1] || ''}${source.slice(inline.index + inline[0].length)}`.replace(/\s+/g, ' ').trim(),
24
+ };
25
+ }
26
+ const split = new RegExp(`(^|\\s)${flagName}\\s+([^\\s]+)(?=\\s|$)`).exec(source);
27
+ if (split) {
28
+ return {
29
+ value: split[2],
30
+ text: `${source.slice(0, split.index)}${split[1] || ''}${source.slice(split.index + split[0].length)}`.replace(/\s+/g, ' ').trim(),
31
+ };
32
+ }
33
+ return { value: null, text: source.trim() };
34
+ }
35
+
36
+ function stripBooleanFlag(text, flagName) {
37
+ return String(text || '').replace(new RegExp(`(^|\\s)${flagName}(?=\\s|$)`, 'g'), ' ').replace(/\s+/g, ' ').trim();
38
+ }
39
+
40
+ function hasBooleanFlag(text, flagName) {
41
+ return new RegExp(`(^|\\s)${flagName}(?=\\s|$)`).test(String(text || ''));
42
+ }
43
+
44
+ function missionRunIntentFromMessage(message) {
45
+ const text = String(message || '').trim();
46
+ const match = /^atris\s+mission\s+run(?:\s+([\s\S]+))?$/i.exec(text);
47
+ if (!match) return null;
48
+
49
+ let rest = String(match[1] || '').trim();
50
+ const ownerFlag = extractMissionFlag(rest, '--owner');
51
+ rest = ownerFlag.text;
52
+ const maxTicksFlag = extractMissionFlag(rest, '--max-ticks');
53
+ rest = maxTicksFlag.text;
54
+ const maxWallFlag = extractMissionFlag(rest, '--max-wall');
55
+ rest = maxWallFlag.text;
56
+ const cadenceFlag = extractMissionFlag(rest, '--cadence');
57
+ rest = cadenceFlag.text;
58
+ const noRun = hasBooleanFlag(rest, '--no-run') || hasBooleanFlag(rest, '--no-loop');
59
+ const noComplete = hasBooleanFlag(rest, '--no-complete');
60
+ rest = stripBooleanFlag(stripBooleanFlag(stripBooleanFlag(rest, '--no-run'), '--no-loop'), '--no-complete');
61
+ rest = rest.replace(/(^|\s)--json(?=\s|$)/g, ' ').replace(/\s+/g, ' ').trim();
62
+
63
+ const wantsLongRun = /\b(24\s*h|24\s*hours?|overnight|forever)\b/i.test(text);
64
+ return {
65
+ objective: stripMissionQuotes(rest),
66
+ owner: ownerFlag.value || process.env.ATRIS_AGENT_ID || 'mission-lead',
67
+ maxTicks: positiveInteger(maxTicksFlag.value, wantsLongRun ? 96 : 1),
68
+ maxWallSeconds: positiveInteger(maxWallFlag.value, wantsLongRun ? 86400 : 300),
69
+ cadence: cadenceFlag.value || (wantsLongRun ? '15m' : ''),
70
+ noRun,
71
+ noComplete,
72
+ };
73
+ }
74
+
75
+ function positiveInteger(value, fallback) {
76
+ const parsed = Number(value);
77
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
78
+ return fallback;
79
+ }
80
+
81
+ function parseJson(stdout) {
82
+ const text = String(stdout || '').trim();
83
+ if (!text) return null;
84
+ return JSON.parse(text);
85
+ }
86
+
87
+ function runCliJsonSync(cliPath, args, options = {}) {
88
+ const result = spawnSync(process.execPath, [cliPath, ...args], {
89
+ cwd: options.cwd || process.cwd(),
90
+ encoding: 'utf8',
91
+ timeout: options.timeoutMs || 30000,
92
+ env: {
93
+ ...process.env,
94
+ ATRIS_SKIP_UPDATE_CHECK: '1',
95
+ ...(options.env || {}),
96
+ },
97
+ });
98
+ if (result.error) {
99
+ return { ok: false, status: 1, error: result.error.message || String(result.error), stdout: result.stdout || '', stderr: result.stderr || '' };
100
+ }
101
+ let payload = null;
102
+ try { payload = parseJson(result.stdout); } catch (error) {
103
+ return { ok: false, status: result.status || 1, error: `invalid JSON: ${error.message}`, stdout: result.stdout || '', stderr: result.stderr || '' };
104
+ }
105
+ return { ok: result.status === 0 && payload?.ok !== false, status: result.status || 0, payload, stdout: result.stdout || '', stderr: result.stderr || '' };
106
+ }
107
+
108
+ function runCliJsonStreaming(cliPath, args, options = {}) {
109
+ return new Promise((resolve) => {
110
+ const startedAt = Date.now();
111
+ const proc = spawn(process.execPath, [cliPath, ...args], {
112
+ cwd: options.cwd || process.cwd(),
113
+ env: {
114
+ ...process.env,
115
+ ATRIS_SKIP_UPDATE_CHECK: '1',
116
+ ...(options.env || {}),
117
+ },
118
+ stdio: ['ignore', 'pipe', 'pipe'],
119
+ });
120
+ let stdout = '';
121
+ let stderr = '';
122
+ let done = false;
123
+ let killedByTimeout = false;
124
+ let progressTimer = null;
125
+ const finish = (status) => {
126
+ if (done) return;
127
+ done = true;
128
+ clearTimeout(timer);
129
+ if (progressTimer) clearInterval(progressTimer);
130
+ let payload = null;
131
+ let error = '';
132
+ try { payload = parseJson(stdout); } catch (err) { error = `invalid JSON: ${err.message}`; }
133
+ resolve({
134
+ ok: status === 0 && !error && payload?.ok !== false,
135
+ status: status || 0,
136
+ payload,
137
+ stdout,
138
+ stderr,
139
+ error: killedByTimeout ? 'timeout' : error,
140
+ });
141
+ };
142
+ const timer = setTimeout(() => {
143
+ killedByTimeout = true;
144
+ try { proc.kill('SIGTERM'); } catch {}
145
+ setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 3000).unref();
146
+ }, options.timeoutMs || 660000);
147
+ progressTimer = options.onProgress
148
+ ? setInterval(() => {
149
+ options.onProgress(`still_running ${Math.max(1, Math.round((Date.now() - startedAt) / 1000))}`);
150
+ }, options.progressEveryMs || 15000)
151
+ : null;
152
+ if (progressTimer?.unref) progressTimer.unref();
153
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
154
+ proc.stderr.on('data', (chunk) => {
155
+ const text = chunk.toString();
156
+ stderr += text;
157
+ if (options.onProgress) {
158
+ for (const line of text.split(/\r?\n/).map((item) => item.trim()).filter(Boolean)) {
159
+ options.onProgress(line);
160
+ }
161
+ }
162
+ });
163
+ proc.on('error', (error) => {
164
+ stderr += error.message || String(error);
165
+ finish(1);
166
+ });
167
+ proc.on('close', finish);
168
+ });
169
+ }
170
+
171
+ function missionGoalFromPayload(payload) {
172
+ const mission = payload?.mission || {};
173
+ const goal = payload?.atris_goal_state?.goal || {};
174
+ return {
175
+ objective: goal.objective || mission.objective,
176
+ mission_id: goal.mission_id || mission.id,
177
+ mission_status: goal.mission_status || mission.status,
178
+ runner: goal.runner || mission.runner || 'atris2',
179
+ next_command: goal.next_command || payload?.next_command || mission.next_action,
180
+ };
181
+ }
182
+
183
+ function firstUsefulLine(text, fallback = '') {
184
+ return String(text || '')
185
+ .split(/\r?\n/)
186
+ .map((line) => line.replace(/^[-*\s#]+/, '').trim())
187
+ .filter(Boolean)
188
+ .find((line) => !/^(receipt|summary|final|final answer|result)$/i.test(line))
189
+ || fallback;
190
+ }
191
+
192
+ function lastTickFromRun(runPayload) {
193
+ const ticks = Array.isArray(runPayload?.ticks) ? runPayload.ticks : [];
194
+ return ticks[ticks.length - 1] || null;
195
+ }
196
+
197
+ function workerLine(runPayload) {
198
+ const tick = lastTickFromRun(runPayload);
199
+ if (!tick) return '';
200
+ if (tick.atris2) return firstUsefulLine(tick.atris2.receipt_text, tick.atris2.ok ? 'Atris2 worker ran.' : 'Atris2 worker failed.');
201
+ if (tick.claude) return firstUsefulLine(tick.claude.receipt_text || tick.summary, 'Worker tick recorded.');
202
+ return tick.summary || tick.reason || '';
203
+ }
204
+
205
+ function formatRuntimeMissionLoopReceipt(result) {
206
+ const lines = [];
207
+ const goal = missionGoalFromPayload(result.start?.payload || {});
208
+ if (result.start?.payload) {
209
+ lines.push('Atris mission started');
210
+ lines.push(`Goal: ${goal.objective || '?'}`);
211
+ lines.push(`Mission: ${goal.mission_id || '?'} - ${goal.mission_status || '?'} - ${goal.runner || 'atris2'}`);
212
+ }
213
+ if (result.noRun) {
214
+ lines.push('Pursuing: not run (--no-run)');
215
+ lines.push('Achieved: no');
216
+ if (goal.next_command) lines.push(`Next: ${goal.next_command}`);
217
+ return lines.join('\n');
218
+ }
219
+ lines.push('Atris mission pursued');
220
+ if (result.run?.payload) {
221
+ const ran = Number(result.run.payload.ran_ticks || 0);
222
+ const total = Number(result.run.payload.tick_count || 0);
223
+ lines.push(`Ticks: ${ran}/${total}`);
224
+ const summary = workerLine(result.run.payload);
225
+ if (summary) lines.push(`Work: ${summary}`);
226
+ }
227
+ lines.push(`Achieved: ${result.achieved ? 'yes' : 'no'}`);
228
+ if (result.complete?.payload?.landing?.happened) lines.push(`Landing: ${result.complete.payload.landing.happened}`);
229
+ else if (result.run?.payload?.pause_reason) lines.push(`Blocked: ${result.run.payload.pause_reason}`);
230
+ else if (result.error) lines.push(`Blocked: ${result.error}`);
231
+ const proof = result.complete?.payload?.mission?.proof || result.run?.payload?.summary_receipt || result.start?.payload?.state_path;
232
+ if (proof) lines.push(`Proof: ${proof}`);
233
+ const nextGoal = result.complete?.payload?.continuation_goal?.mission?.objective;
234
+ if (nextGoal) lines.push(`Next goal: ${nextGoal}`);
235
+ const nextCommand = result.complete?.payload?.atris_goal_state?.goal?.next_command || result.run?.payload?.atris_goal_state?.goal?.next_command || goal.next_command;
236
+ if (nextCommand && !nextGoal) lines.push(`Next: ${nextCommand}`);
237
+ return lines.join('\n');
238
+ }
239
+
240
+ async function runRuntimeMissionLoop(intent, options = {}) {
241
+ const cliPath = options.cliPath;
242
+ const cwd = options.cwd || process.cwd();
243
+ if (!cliPath) return { ok: false, status: 1, error: 'missing cliPath', text: 'Blocked: missing cliPath' };
244
+ if (!intent?.objective) return { ok: false, status: 1, error: 'missing objective', text: 'Usage: atris mission run "<objective>"' };
245
+
246
+ const startArgs = ['mission', 'run', intent.objective, '--runner', 'atris2', '--owner', intent.owner || 'mission-lead', '--json'];
247
+ if (intent.cadence) startArgs.push('--cadence', intent.cadence);
248
+ const start = runCliJsonSync(cliPath, startArgs, { cwd, timeoutMs: 30000 });
249
+ const result = { ok: start.ok, status: start.status, start, noRun: intent.noRun === true, achieved: false };
250
+ if (!start.ok || !start.payload?.mission?.id) {
251
+ result.error = start.error || start.payload?.error || 'mission start failed';
252
+ result.text = formatRuntimeMissionLoopReceipt(result);
253
+ return result;
254
+ }
255
+ if (intent.noRun) {
256
+ result.text = formatRuntimeMissionLoopReceipt(result);
257
+ return result;
258
+ }
259
+
260
+ if (options.onProgress) options.onProgress(`pursuing ${start.payload.mission.id}`);
261
+ const maxWallSeconds = positiveInteger(intent.maxWallSeconds, 600);
262
+ const run = await runCliJsonStreaming(
263
+ cliPath,
264
+ ['mission', 'run', start.payload.mission.id, '--max-ticks', String(positiveInteger(intent.maxTicks, 1)), '--max-wall', String(maxWallSeconds), '--complete-on-pass', '--json'],
265
+ {
266
+ cwd,
267
+ timeoutMs: (maxWallSeconds + 60) * 1000,
268
+ onProgress: options.onProgress,
269
+ },
270
+ );
271
+ result.run = run;
272
+ if (!run.ok) {
273
+ result.ok = false;
274
+ result.status = run.status || 1;
275
+ result.error = run.error || run.payload?.error || 'mission run failed';
276
+ result.text = formatRuntimeMissionLoopReceipt(result);
277
+ return result;
278
+ }
279
+
280
+ if (run.payload?.mission?.status === 'complete') {
281
+ result.achieved = true;
282
+ result.ok = true;
283
+ result.text = formatRuntimeMissionLoopReceipt(result);
284
+ return result;
285
+ }
286
+
287
+ const mission = run.payload?.mission || {};
288
+ const ranTicks = Number(run.payload?.ran_ticks || 0);
289
+ const proof = run.payload?.summary_receipt || mission.receipt_path;
290
+ if (!intent.noComplete && ranTicks > 0 && !String(mission.verifier || '').trim() && proof) {
291
+ if (options.onProgress) options.onProgress(`landing ${mission.id}`);
292
+ const complete = runCliJsonSync(cliPath, ['mission', 'complete', mission.id, '--proof', proof, '--json'], { cwd, timeoutMs: 30000 });
293
+ result.complete = complete;
294
+ result.achieved = complete.ok && complete.payload?.action === 'mission_completed';
295
+ result.ok = result.achieved;
296
+ if (!complete.ok) result.error = complete.error || complete.payload?.error || 'mission complete failed';
297
+ } else {
298
+ result.ok = false;
299
+ result.error = run.payload?.pause_reason || (ranTicks > 0 ? 'mission still running' : 'no tick proof');
300
+ }
301
+ result.text = formatRuntimeMissionLoopReceipt(result);
302
+ return result;
303
+ }
304
+
305
+ function readAtrisGoalState(cwd = process.cwd()) {
306
+ try {
307
+ const file = path.join(cwd, '.atris', 'state', 'atris_goal.json');
308
+ if (!fs.existsSync(file)) return null;
309
+ return JSON.parse(fs.readFileSync(file, 'utf8'))?.goal || null;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+
315
+ module.exports = {
316
+ missionRunIntentFromMessage,
317
+ runRuntimeMissionLoop,
318
+ formatRuntimeMissionLoopReceipt,
319
+ readAtrisGoalState,
320
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.30.7",
3
+ "version": "3.30.12",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",