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.
- package/GETTING_STARTED.md +65 -131
- package/README.md +18 -2
- package/atris/GETTING_STARTED.md +65 -131
- package/atris/PERSONA.md +5 -1
- package/atris/atris.md +122 -153
- package/atris/skills/aeo/SKILL.md +117 -0
- package/atris/skills/atris/SKILL.md +49 -25
- package/atris/skills/create-member/SKILL.md +29 -9
- package/atris/skills/endgame/SKILL.md +9 -0
- package/atris/skills/research-search/SKILL.md +167 -0
- package/atris/skills/research-search/arxiv_search.py +157 -0
- package/atris/skills/research-search/program.md +48 -0
- package/atris/skills/research-search/results.tsv +6 -0
- package/atris/skills/research-search/scholar_search.py +154 -0
- package/atris/skills/tidy/SKILL.md +36 -21
- package/atris/team/_template/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +35 -1
- package/atris.md +118 -178
- package/bin/atris.js +30 -5
- package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
- package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
- package/cli/atris_code.py +889 -0
- package/cli/runtime_guard.py +693 -0
- package/commands/align.js +15 -0
- package/commands/app.js +316 -0
- package/commands/autopilot.js +390 -7
- package/commands/business.js +677 -2
- package/commands/computer.js +1979 -43
- package/commands/context-sync.js +5 -0
- package/commands/lifecycle.js +12 -0
- package/commands/plugin.js +24 -0
- package/commands/pull.js +40 -1
- package/commands/push.js +44 -0
- package/commands/serve.js +1 -0
- package/commands/sync.js +272 -76
- package/commands/verify.js +50 -1
- package/commands/wiki.js +27 -2
- package/lib/file-ops.js +13 -1
- package/lib/journal.js +23 -0
- package/lib/scorecard.js +42 -4
- package/lib/sync-telemetry.js +59 -0
- package/lib/todo.js +6 -0
- package/lib/wiki.js +150 -6
- package/package.json +2 -1
- package/utils/api.js +19 -0
- package/utils/auth.js +25 -1
- package/utils/config.js +24 -0
- package/utils/update-check.js +16 -0
package/commands/context-sync.js
CHANGED
|
@@ -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();
|
package/commands/lifecycle.js
CHANGED
|
@@ -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
|
|
package/commands/plugin.js
CHANGED
|
@@ -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)
|
|
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 };
|