atris 3.2.0 → 3.5.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 (49) hide show
  1. package/GETTING_STARTED.md +65 -131
  2. package/README.md +18 -2
  3. package/atris/GETTING_STARTED.md +65 -131
  4. package/atris/PERSONA.md +5 -1
  5. package/atris/atris.md +122 -153
  6. package/atris/skills/aeo/SKILL.md +117 -0
  7. package/atris/skills/atris/SKILL.md +49 -25
  8. package/atris/skills/create-member/SKILL.md +29 -9
  9. package/atris/skills/endgame/SKILL.md +9 -0
  10. package/atris/skills/research-search/SKILL.md +167 -0
  11. package/atris/skills/research-search/arxiv_search.py +157 -0
  12. package/atris/skills/research-search/program.md +48 -0
  13. package/atris/skills/research-search/results.tsv +6 -0
  14. package/atris/skills/research-search/scholar_search.py +154 -0
  15. package/atris/skills/tidy/SKILL.md +36 -21
  16. package/atris/team/_template/MEMBER.md +2 -0
  17. package/atris/team/validator/MEMBER.md +35 -1
  18. package/atris.md +118 -178
  19. package/bin/atris.js +30 -5
  20. package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
  21. package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
  22. package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
  23. package/cli/atris_code.py +889 -0
  24. package/cli/runtime_guard.py +693 -0
  25. package/commands/align.js +15 -0
  26. package/commands/app.js +316 -0
  27. package/commands/autopilot.js +390 -7
  28. package/commands/business.js +677 -2
  29. package/commands/computer.js +1979 -43
  30. package/commands/context-sync.js +5 -0
  31. package/commands/lifecycle.js +12 -0
  32. package/commands/plugin.js +24 -0
  33. package/commands/pull.js +40 -1
  34. package/commands/push.js +44 -0
  35. package/commands/serve.js +1 -0
  36. package/commands/sync.js +272 -76
  37. package/commands/verify.js +50 -1
  38. package/commands/wiki.js +27 -2
  39. package/lib/file-ops.js +13 -1
  40. package/lib/journal.js +23 -0
  41. package/lib/scorecard.js +42 -4
  42. package/lib/sync-telemetry.js +59 -0
  43. package/lib/todo.js +6 -0
  44. package/lib/wiki.js +150 -6
  45. package/package.json +2 -1
  46. package/utils/api.js +19 -0
  47. package/utils/auth.js +25 -1
  48. package/utils/config.js +24 -0
  49. package/utils/update-check.js +16 -0
@@ -281,6 +281,11 @@ async function businessLog(slug) {
281
281
  }
282
282
 
283
283
 
284
+ /**
285
+ * Convert ISO timestamp to human-readable relative time (e.g., "5m ago").
286
+ * @param {string} isoString - ISO 8601 timestamp
287
+ * @returns {string|null} Relative time string, or null if no input
288
+ */
284
289
  function _timeSince(isoString) {
285
290
  if (!isoString) return null;
286
291
  const diff = Date.now() - new Date(isoString).getTime();
@@ -3,6 +3,10 @@ const path = require('path');
3
3
  const { loadCredentials } = require('../utils/auth');
4
4
  const { apiRequestJson } = require('../utils/api');
5
5
 
6
+ /**
7
+ * Resolve business slug from CLI arg or local .atris/business.json.
8
+ * @returns {string|null} The resolved slug, or null if not found.
9
+ */
6
10
  function resolveSlug() {
7
11
  let slug = process.argv[3];
8
12
  if (!slug || slug.startsWith('-')) {
@@ -18,6 +22,10 @@ function resolveSlug() {
18
22
  return slug;
19
23
  }
20
24
 
25
+ /**
26
+ * Pause a workspace to save compute (storage only mode).
27
+ * @returns {Promise<void>}
28
+ */
21
29
  async function sleepAtris() {
22
30
  const slug = resolveSlug();
23
31
 
@@ -45,6 +53,10 @@ async function sleepAtris() {
45
53
  console.log('Compute paused. Storage only — pennies/day.');
46
54
  }
47
55
 
56
+ /**
57
+ * Wake a sleeping workspace so agents resume automatically.
58
+ * @returns {Promise<void>}
59
+ */
48
60
  async function wakeAtris() {
49
61
  const slug = resolveSlug();
50
62
 
@@ -6,6 +6,11 @@ const { findAllSkills, parseFrontmatter } = require('./skill');
6
6
 
7
7
  // --- Recursive Copy Helper ---
8
8
 
9
+ /**
10
+ * Recursively copy a directory, skipping .DS_Store and .git.
11
+ * @param {string} src - Source directory path
12
+ * @param {string} dest - Destination directory path
13
+ */
9
14
  function copyRecursive(src, dest) {
10
15
  fs.mkdirSync(dest, { recursive: true });
11
16
  const entries = fs.readdirSync(src);
@@ -23,6 +28,12 @@ function copyRecursive(src, dest) {
23
28
 
24
29
  // --- Generate plugin.json manifest ---
25
30
 
31
+ /**
32
+ * Generate plugin.json manifest from discovered skills.
33
+ * @param {Array} skills - Array of skill objects
34
+ * @param {string} projectDir - Project root directory
35
+ * @returns {Object} Plugin manifest object
36
+ */
26
37
  function generateManifest(skills, projectDir) {
27
38
  let pkg = {};
28
39
  const pkgPath = path.join(projectDir, 'package.json');
@@ -44,6 +55,10 @@ function generateManifest(skills, projectDir) {
44
55
 
45
56
  // --- Generate /atris-setup command ---
46
57
 
58
+ /**
59
+ * Generate the /atris-setup skill markdown for workspace bootstrapping.
60
+ * @returns {string} Skill markdown content
61
+ */
47
62
  function generateSetupCommand() {
48
63
  return `---
49
64
  description: Set up Atris authentication and connect integrations (Gmail, Calendar, Slack, Notion, Drive)
@@ -113,6 +128,11 @@ Tell the user which integrations are connected and which still need setup. They
113
128
 
114
129
  // --- Generate README.md ---
115
130
 
131
+ /**
132
+ * Generate README.md for the plugin package.
133
+ * @param {Array} skills - Array of discovered skills
134
+ * @returns {string} README markdown content
135
+ */
116
136
  function generateREADME(skills) {
117
137
  const skillList = skills.map(s => {
118
138
  const fm = s.frontmatter || {};
@@ -149,6 +169,10 @@ ${skillList}
149
169
 
150
170
  // --- BUILD subcommand ---
151
171
 
172
+ /**
173
+ * Build the plugin package from atris/skills into dist/.
174
+ * @param {...string} args - CLI arguments
175
+ */
152
176
  function buildPlugin(...args) {
153
177
  const projectDir = process.cwd();
154
178
  const atrisDir = path.join(projectDir, 'atris');
package/commands/pull.js CHANGED
@@ -9,6 +9,7 @@ const { parseJournalSections, mergeSections, reconstructJournal } = require('../
9
9
  const { loadBusinesses } = require('./business');
10
10
  const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare } = require('../lib/manifest');
11
11
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
12
+ const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
12
13
 
13
14
  function pruneEmptyParentDirs(filePath, stopDir) {
14
15
  let current = path.dirname(filePath);
@@ -114,6 +115,7 @@ async function pullAtris() {
114
115
 
115
116
 
116
117
  async function pullBusiness(slug) {
118
+ const elapsedMs = startTimer();
117
119
  const creds = loadCredentials();
118
120
  if (!creds || !creds.token) {
119
121
  console.error('Not logged in. Run: atris login');
@@ -223,6 +225,11 @@ async function pullBusiness(slug) {
223
225
  process.exit(1);
224
226
  }
225
227
 
228
+ // Telemetry helper — captures wall-clock time including wake.
229
+ let _coldWake = false;
230
+ const emit = (outcome, extras = {}) =>
231
+ emitSyncEvent(creds.token, businessId, workspaceId, 'pull', outcome, elapsedMs(), extras);
232
+
226
233
  // Auto-wake the EC2 computer if --auto-wake is set.
227
234
  // Without this, pull silently serves stale data from agent_files cache when
228
235
  // the computer is asleep — the bug that confused us all night.
@@ -232,6 +239,7 @@ async function pullBusiness(slug) {
232
239
  const computerStatus = statusResult.ok && statusResult.data ? statusResult.data.status : 'unknown';
233
240
  if (computerStatus !== 'running' || !(statusResult.data && statusResult.data.endpoint)) {
234
241
  process.stdout.write(' Waking EC2 computer... ');
242
+ _coldWake = true;
235
243
  await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
236
244
  const wakeStart = Date.now();
237
245
  while (Date.now() - wakeStart < 90000) {
@@ -355,20 +363,30 @@ async function pullBusiness(slug) {
355
363
 
356
364
  if (!result.ok) {
357
365
  const msg = result.errorMessage || result.error || `HTTP ${result.status}`;
366
+ let outcome = 'status_unknown';
358
367
  if (result.status === 0 || (typeof msg === 'string' && msg.toLowerCase().includes('timeout'))) {
359
368
  console.error(`\n Workspace timed out (large workspaces can take 60s+). Try: atris pull ${slug} --timeout=600`);
369
+ outcome = 'hang';
360
370
  } else if (result.status === 502) {
361
371
  console.error(`\n Computer didn't respond in time. It may be waking up or the workspace is large.`);
362
372
  console.error(` Try again in 30s, or use: atris pull ${slug} --only=team/,context/`);
373
+ outcome = 'cold_wake';
363
374
  } else if (result.status === 409) {
364
375
  console.error(`\n Computer is sleeping. Wake it first, then pull again.`);
376
+ outcome = 'cold_wake';
365
377
  } else if (result.status === 403) {
366
378
  console.error(`\n Access denied. You're not a member of "${slug}".`);
379
+ outcome = 'access_denied';
367
380
  } else if (result.status === 404) {
368
381
  console.error(`\n Business "${slug}" not found.`);
382
+ outcome = 'status_unknown';
383
+ } else if (result.status === 429) {
384
+ console.error(`\n Pull failed: ${msg}`);
385
+ outcome = 'rate_limited';
369
386
  } else {
370
387
  console.error(`\n Pull failed: ${msg}`);
371
388
  }
389
+ await emit(outcome, { error_detail: `${result.status}: ${msg}`.slice(0, 500) });
372
390
  process.exit(1);
373
391
  }
374
392
 
@@ -379,7 +397,10 @@ async function pullBusiness(slug) {
379
397
  // mirror sweep so a genuinely-emptied cloud can clear local files. The
380
398
  // sweep itself has a safety guard that refuses to wipe local content
381
399
  // when remote reports empty (the snapshot-glitch case), so this is safe.
382
- if (!force) return;
400
+ if (!force) {
401
+ await emit(_coldWake ? 'cold_wake' : 'success', { files_unchanged: 0 });
402
+ return;
403
+ }
383
404
  } else {
384
405
  console.log(` Processing ${files.length} files...`);
385
406
  }
@@ -702,6 +723,24 @@ async function pullBusiness(slug) {
702
723
  }
703
724
  }
704
725
 
726
+ // Telemetry — only count files where content actually came over the wire
727
+ // (smart-pull skips unchanged files and just sends a hash). Counting all
728
+ // remoteFiles here would dwarf the real signal once smart-pull steady-state
729
+ // is normal (< 1% of files transferred per pull).
730
+ let bytesTransferred = 0;
731
+ let filesTransferred = 0;
732
+ for (const filePath of Object.keys(remoteContent)) {
733
+ const f = remoteFiles[filePath];
734
+ if (f && typeof f.size === 'number') bytesTransferred += f.size;
735
+ filesTransferred++;
736
+ }
737
+ const totalRemote = Object.keys(remoteFiles).length;
738
+ await emit(_coldWake ? 'cold_wake' : 'success', {
739
+ files_pushed: filesTransferred,
740
+ files_unchanged: Math.max(0, totalRemote - filesTransferred),
741
+ bytes_transferred: bytesTransferred,
742
+ bytes_changed: bytesTransferred,
743
+ });
705
744
  }
706
745
 
707
746
 
package/commands/push.js CHANGED
@@ -6,9 +6,12 @@ const { apiRequestJson } = require('../utils/api');
6
6
  const { loadBusinesses, saveBusinesses } = require('./business');
7
7
  const { loadManifest, saveManifest, buildManifest, computeLocalHashes } = require('../lib/manifest');
8
8
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
9
+ const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
9
10
 
10
11
  async function pushAtris() {
12
+ const elapsedMs = startTimer();
11
13
  let slug = process.argv[3];
14
+ let _coldWake = false;
12
15
 
13
16
  // Auto-detect business from .atris/business.json in current dir
14
17
  if (!slug || slug.startsWith('-')) {
@@ -105,6 +108,11 @@ async function pushAtris() {
105
108
 
106
109
  if (!workspaceId) { console.error(`Business "${slug}" has no workspace.`); process.exit(1); }
107
110
 
111
+ // Telemetry helper — emits one event with the elapsed wall-clock time.
112
+ // Awaited (not fire-and-forget) because process.exit kills in-flight requests.
113
+ const emit = (outcome, extras = {}) =>
114
+ emitSyncEvent(creds.token, businessId, workspaceId, 'push', outcome, elapsedMs(), extras);
115
+
108
116
  // Auto-wake the EC2 computer if --auto-wake is set, otherwise check status and warn.
109
117
  // Without this, push silently routes to agent_files cache when computer is asleep
110
118
  // (the silent fallback footgun from tonight's debugging).
@@ -114,6 +122,7 @@ async function pushAtris() {
114
122
  const computerStatus = statusResult.ok && statusResult.data ? statusResult.data.status : 'unknown';
115
123
  if (computerStatus !== 'running' || !(statusResult.data && statusResult.data.endpoint)) {
116
124
  process.stdout.write(' Waking EC2 computer... ');
125
+ _coldWake = true;
117
126
  await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token: creds.token });
118
127
  const wakeStart = Date.now();
119
128
  while (Date.now() - wakeStart < 90000) {
@@ -204,6 +213,7 @@ async function pushAtris() {
204
213
  console.log('');
205
214
  console.log(' Run `atris pull` first, then push your changes.');
206
215
  console.log(' To override (force-push, may clobber cloud edits): atris push --force');
216
+ await emit('drift', { error_detail: `${driftFiles.length} file(s) drifted` });
207
217
  process.exit(1);
208
218
  }
209
219
  console.log('fresh');
@@ -218,6 +228,7 @@ async function pushAtris() {
218
228
  console.log(' ✗ Could not verify cloud freshness. Refusing to push.');
219
229
  console.log(' The workspace may be unreachable or the snapshot endpoint is broken.');
220
230
  console.log(' To bypass and force-push anyway: atris push --force');
231
+ await emit('status_unknown', { error_detail: `snapshot status ${snapshotResult.status || 'unknown'}` });
221
232
  process.exit(1);
222
233
  }
223
234
  }
@@ -256,6 +267,7 @@ async function pushAtris() {
256
267
 
257
268
  if (filesToPush.length === 0 && deletedPaths.length === 0) {
258
269
  console.log('\n Already up to date.\n');
270
+ await emit('success', { files_unchanged: filteredLocalCount });
259
271
  return;
260
272
  }
261
273
 
@@ -304,17 +316,21 @@ async function pushAtris() {
304
316
  pushed = allowed.length;
305
317
  } else {
306
318
  console.error(`\n Push failed: ${retry.errorMessage || retry.error || retry.status}`);
319
+ await emit('access_denied', { error_detail: `403 retry failed: ${retry.status}` });
307
320
  process.exit(1);
308
321
  }
309
322
  } else {
310
323
  console.error('\n Access denied: you can only push to your team/ folder.');
324
+ await emit('access_denied');
311
325
  process.exit(1);
312
326
  }
313
327
  } else if (result.status === 409) {
314
328
  console.error('\n Computer is sleeping. Wake it first.');
329
+ await emit('cold_wake', { error_detail: 'computer sleeping (409)' });
315
330
  process.exit(1);
316
331
  } else {
317
332
  console.error(`\n Push failed: ${result.errorMessage || result.error || result.status}`);
333
+ await emit('status_unknown', { error_detail: `sync status ${result.status}` });
318
334
  process.exit(1);
319
335
  }
320
336
  } else {
@@ -333,6 +349,7 @@ async function pushAtris() {
333
349
  // - manifest update only counts confirmed-deleted paths
334
350
  const deletedConfirmed = [];
335
351
  const deleteFailed = [];
352
+ let _rateLimitedDeletes = 0;
336
353
  for (let i = 0; i < deletedPaths.length; i++) {
337
354
  const filePath = deletedPaths[i];
338
355
  if (i > 0) {
@@ -345,6 +362,7 @@ async function pushAtris() {
345
362
  );
346
363
  if (deleteResult.status === 429) {
347
364
  // Rate limit — wait 20s, retry once
365
+ _rateLimitedDeletes++;
348
366
  await new Promise((r) => setTimeout(r, 20000));
349
367
  deleteResult = await apiRequestJson(
350
368
  `/business/${businessId}/workspaces/${workspaceId}/file?path=${encodeURIComponent(filePath)}`,
@@ -406,6 +424,32 @@ async function pushAtris() {
406
424
  delete updatedFiles[filePath];
407
425
  }
408
426
  saveManifest(resolvedSlug || slug, buildManifest(updatedFiles, null));
427
+
428
+ // Telemetry — outcome reflects actual run quality, not just exit-code-zero.
429
+ // Partial delete failures or rate-limit retries mean the run was NOT a clean win;
430
+ // labeling them success would poison the RL signal.
431
+ const bytesChanged = filesToPush.reduce((acc, f) => acc + (f.content ? Buffer.byteLength(f.content, 'utf8') : 0), 0);
432
+ let finalOutcome;
433
+ let finalDetail;
434
+ if (deleteFailed.length > 0) {
435
+ finalOutcome = 'status_unknown';
436
+ finalDetail = `${deleteFailed.length} delete(s) failed (statuses: ${[...new Set(deleteFailed.map(f => f.status))].join(',')})`;
437
+ } else if (_rateLimitedDeletes > 0) {
438
+ finalOutcome = 'rate_limited';
439
+ finalDetail = `${_rateLimitedDeletes} delete(s) hit 429 (recovered)`;
440
+ } else if (_coldWake) {
441
+ finalOutcome = 'cold_wake';
442
+ } else {
443
+ finalOutcome = 'success';
444
+ }
445
+ await emit(finalOutcome, {
446
+ files_pushed: pushed,
447
+ files_deleted: deleted,
448
+ files_unchanged: unchangedCount,
449
+ bytes_changed: bytesChanged,
450
+ bytes_transferred: bytesChanged,
451
+ error_detail: finalDetail,
452
+ });
409
453
  }
410
454
 
411
455
  module.exports = { pushAtris };
package/commands/serve.js CHANGED
@@ -354,6 +354,7 @@ async function serveAtris(options = {}) {
354
354
 
355
355
  module.exports = {
356
356
  serveAtris,
357
+ streamSession,
357
358
  // exported for testing
358
359
  safePath,
359
360
  applyOp,