atris 3.13.0 → 3.14.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.
@@ -5,6 +5,7 @@
5
5
  * atris computer --cloud — Open CLOUD workspace mode
6
6
  * atris computer wake — Start the computer
7
7
  * atris computer sleep — Stop (files persist)
8
+ * atris computer card — Show the local computer card
8
9
  * atris computer run <command> — Run bash on EC2 (no LLM)
9
10
  * atris computer grep <pattern> — Search files on EC2
10
11
  * atris computer ls [path] — List files
@@ -705,8 +706,8 @@ async function runLocalBridgeOp(token, sessionId, op, timeoutSeconds = 30) {
705
706
  return data;
706
707
  }
707
708
 
708
- function readBusinessBinding() {
709
- const bindingPath = path.join(process.cwd(), '.atris', 'business.json');
709
+ function readBusinessBinding(cwd = process.cwd()) {
710
+ const bindingPath = path.join(cwd, '.atris', 'business.json');
710
711
  if (!fs.existsSync(bindingPath)) return null;
711
712
  try {
712
713
  return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
@@ -715,6 +716,160 @@ function readBusinessBinding() {
715
716
  }
716
717
  }
717
718
 
719
+ function readPackageMeta(cwd = process.cwd()) {
720
+ const packagePath = path.join(cwd, 'package.json');
721
+ if (!fs.existsSync(packagePath)) return null;
722
+ try {
723
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
724
+ } catch {
725
+ return null;
726
+ }
727
+ }
728
+
729
+ function relIfExists(cwd, target) {
730
+ return fs.existsSync(path.join(cwd, target)) ? target : null;
731
+ }
732
+
733
+ function detectValidationCommand(cwd = process.cwd(), pkg = null) {
734
+ const meta = pkg || readPackageMeta(cwd);
735
+ const testScript = meta?.scripts?.test;
736
+ if (testScript && !/no test specified/i.test(testScript)) return 'npm test';
737
+ if (fs.existsSync(path.join(cwd, 'pytest.ini')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) return 'pytest';
738
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return 'cargo test';
739
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) return 'go test ./...';
740
+ return 'none detected';
741
+ }
742
+
743
+ function detectComputerType(cwd = process.cwd(), pkg = null, binding = null) {
744
+ if (binding?.computer_type) return binding.computer_type;
745
+ if (binding?.workspace_type) return binding.workspace_type;
746
+ const meta = pkg || readPackageMeta(cwd);
747
+ if (meta?.bin || fs.existsSync(path.join(cwd, 'bin')) || fs.existsSync(path.join(cwd, 'commands'))) return 'codeops';
748
+ if (fs.existsSync(path.join(cwd, 'docs')) || fs.existsSync(path.join(cwd, 'atris', 'wiki'))) return 'research';
749
+ return 'workspace';
750
+ }
751
+
752
+ function buildComputerCard(cwd = process.cwd()) {
753
+ const binding = readBusinessBinding(cwd);
754
+ const pkg = readPackageMeta(cwd);
755
+ const folderName = path.basename(cwd);
756
+ const ownerName = binding?.name || pkg?.name || folderName;
757
+ const ownerType = binding ? 'business' : 'project';
758
+ const computerName = binding?.computer_name || binding?.workspace_name || `${ownerName} computer`;
759
+ const computerType = detectComputerType(cwd, pkg, binding);
760
+ const memory = [
761
+ relIfExists(cwd, 'atris/MAP.md'),
762
+ relIfExists(cwd, 'atris/TODO.md'),
763
+ relIfExists(cwd, 'atris/wiki'),
764
+ relIfExists(cwd, 'atris/logs'),
765
+ ].filter(Boolean);
766
+ const artifacts = [
767
+ fs.existsSync(path.join(cwd, 'atris')) ? 'atris/reports/' : null,
768
+ relIfExists(cwd, '.atris/receipts'),
769
+ ].filter(Boolean);
770
+
771
+ return {
772
+ ownerName,
773
+ ownerType,
774
+ computerName,
775
+ computerType,
776
+ workspace: cwd,
777
+ loop: 'plan -> do -> review',
778
+ memory,
779
+ validation: detectValidationCommand(cwd, pkg),
780
+ proof: binding ? 'atris computer proof' : 'atris proof run',
781
+ visual: 'atris visualize "<prompt>"',
782
+ artifacts,
783
+ generatedAt: new Date().toISOString(),
784
+ };
785
+ }
786
+
787
+ function renderList(items) {
788
+ return items.length ? items.join(', ') : 'none detected';
789
+ }
790
+
791
+ function renderComputerCard(card) {
792
+ return [
793
+ 'Atris Computer Card',
794
+ '',
795
+ ` Owner: ${card.ownerName} (${card.ownerType})`,
796
+ ` Computer: ${card.computerName}`,
797
+ ` Type: ${card.computerType}`,
798
+ ` Workspace: ${card.workspace}`,
799
+ ` Loop: ${card.loop}`,
800
+ ` Memory: ${renderList(card.memory)}`,
801
+ ` Validate: ${card.validation}`,
802
+ ` Proof: ${card.proof}`,
803
+ ` Visual: ${card.visual}`,
804
+ ` Artifacts: ${renderList(card.artifacts)}`,
805
+ ].join('\n');
806
+ }
807
+
808
+ function renderComputerCardMarkdown(card) {
809
+ return [
810
+ '# Atris Computer Card',
811
+ '',
812
+ `Generated: ${card.generatedAt}`,
813
+ '',
814
+ `- Owner: ${card.ownerName} (${card.ownerType})`,
815
+ `- Computer: ${card.computerName}`,
816
+ `- Type: ${card.computerType}`,
817
+ `- Workspace: ${card.workspace}`,
818
+ `- Loop: ${card.loop}`,
819
+ `- Memory: ${renderList(card.memory)}`,
820
+ `- Validate: ${card.validation}`,
821
+ `- Proof: ${card.proof}`,
822
+ `- Visual: ${card.visual}`,
823
+ `- Artifacts: ${renderList(card.artifacts)}`,
824
+ '',
825
+ ].join('\n');
826
+ }
827
+
828
+ function parseComputerCardArgs(args = []) {
829
+ const options = { write: false, out: null, help: false };
830
+ for (let i = 0; i < args.length; i++) {
831
+ const arg = args[i];
832
+ if (arg === '--help' || arg === '-h') options.help = true;
833
+ else if (arg === '--write') options.write = true;
834
+ else if (arg === '--out' && args[i + 1]) options.out = args[++i];
835
+ else if (arg.startsWith('--out=')) options.out = arg.slice('--out='.length);
836
+ }
837
+ return options;
838
+ }
839
+
840
+ function defaultComputerCardPath(cwd = process.cwd()) {
841
+ if (fs.existsSync(path.join(cwd, 'atris'))) {
842
+ return path.join(cwd, 'atris', 'reports', 'computer-card.md');
843
+ }
844
+ return path.join(cwd, 'computer-card.md');
845
+ }
846
+
847
+ function computerCard(args = [], cwd = process.cwd()) {
848
+ const options = parseComputerCardArgs(args);
849
+ if (options.help) {
850
+ console.log('Usage: atris computer card [--write] [--out <path>]');
851
+ console.log('');
852
+ console.log('Show the local owner/computer card for this workspace.');
853
+ return null;
854
+ }
855
+
856
+ const card = buildComputerCard(cwd);
857
+ console.log(renderComputerCard(card));
858
+
859
+ if (options.write || options.out) {
860
+ const outputPath = options.out
861
+ ? (path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out))
862
+ : defaultComputerCardPath(cwd);
863
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
864
+ fs.writeFileSync(outputPath, renderComputerCardMarkdown(card), 'utf8');
865
+ console.log('');
866
+ console.log(`Wrote ${path.relative(cwd, outputPath) || outputPath}`);
867
+ return outputPath;
868
+ }
869
+
870
+ return card;
871
+ }
872
+
718
873
  async function resolveBusinessContext(token) {
719
874
  const binding = readBusinessBinding();
720
875
  if (!binding) return null;
@@ -2397,6 +2552,11 @@ async function runComputer() {
2397
2552
  return;
2398
2553
  }
2399
2554
 
2555
+ if (sub === 'card') {
2556
+ computerCard(args.slice(1));
2557
+ return;
2558
+ }
2559
+
2400
2560
  if (sub === 'claude' || sub === 'codex') {
2401
2561
  computerLocalLegacy(args);
2402
2562
  return;
@@ -2405,6 +2565,15 @@ async function runComputer() {
2405
2565
  if (sub === '--help') {
2406
2566
  console.log('Usage: atris computer [mode|command]');
2407
2567
  console.log('');
2568
+ console.log('Atris computers are persistent AI workspaces for scoped jobs.');
2569
+ console.log('');
2570
+ console.log(' Owner = User | Business');
2571
+ console.log(' Owner has many Computers');
2572
+ console.log(' Computer = workspace + files + tools + secrets + memory + agents + validation');
2573
+ console.log('');
2574
+ console.log('Common types: codeops, research, CRM, reporting, recruiting, event ops, support, business ops.');
2575
+ console.log('A business can be a company, lab, collective, community, artist, team, or project.');
2576
+ console.log('');
2408
2577
  console.log('First use:');
2409
2578
  console.log(' cd ~/arena/atris-business/<business>');
2410
2579
  console.log(' atris computer');
@@ -2412,6 +2581,7 @@ async function runComputer() {
2412
2581
  console.log('');
2413
2582
  console.log('Modes:');
2414
2583
  console.log(' (default) Choose CLOUD vs LOCAL when both are available');
2584
+ console.log(' card Show the local owner/computer card, no login required');
2415
2585
  console.log(' local Open LOCAL Atris mode; cloud brain edits this folder');
2416
2586
  console.log(' proof Run the local-edit + cloud-isolation + audit proof');
2417
2587
  console.log(' local-byo Open LOCAL BYO Claude mode; Anthropic tokens, no cloud audit');
@@ -2443,6 +2613,8 @@ async function runComputer() {
2443
2613
  console.log('');
2444
2614
  console.log('Examples:');
2445
2615
  console.log(' atris computer');
2616
+ console.log(' atris computer card --write');
2617
+ console.log(' atris business init "My Lab" # shared owner + first/default computer');
2446
2618
  console.log(' atris computer proof');
2447
2619
  console.log(' atris computer local');
2448
2620
  console.log(' atris computer codex');
@@ -2522,6 +2694,7 @@ async function runComputer() {
2522
2694
 
2523
2695
  switch (sub) {
2524
2696
  case 'chat': return computerChat(token, ctx, cloudOptions);
2697
+ case 'card': return computerCard(args.slice(1));
2525
2698
  case 'proof': return computerProof(token, ctx, cloudOptions);
2526
2699
  case 'status': return computerStatus(token, ctx);
2527
2700
  case 'wake': return computerWake(token, ctx);
@@ -2544,4 +2717,9 @@ async function runComputer() {
2544
2717
  }
2545
2718
  }
2546
2719
 
2547
- module.exports = { runComputer };
2720
+ module.exports = {
2721
+ runComputer,
2722
+ buildComputerCard,
2723
+ renderComputerCard,
2724
+ renderComputerCardMarkdown,
2725
+ };
package/commands/pull.js CHANGED
@@ -6,7 +6,7 @@ const { findAllMembers } = require('./member');
6
6
  const { loadConfig } = require('../utils/config');
7
7
  const { getLogPath } = require('../lib/file-ops');
8
8
  const { parseJournalSections, mergeSections, reconstructJournal } = require('../lib/journal');
9
- const { loadBusinesses } = require('./business');
9
+ const { loadBusinesses, businessMatchesSlug } = require('./business');
10
10
  const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare } = require('../lib/manifest');
11
11
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
12
12
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
@@ -204,6 +204,7 @@ async function pullBusiness(slug) {
204
204
 
205
205
  // Resolve business ID — always refresh from API to avoid stale workspace_id
206
206
  let businessId, workspaceId, businessName, resolvedSlug;
207
+ let localSlug = slug;
207
208
  const businesses = loadBusinesses();
208
209
 
209
210
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
@@ -220,7 +221,7 @@ async function pullBusiness(slug) {
220
221
  }
221
222
  } else {
222
223
  const match = (listResult.data || []).find(
223
- b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase()
224
+ b => businessMatchesSlug(b, slug, { includeName: true })
224
225
  );
225
226
  if (!match) {
226
227
  console.error(`Business "${slug}" not found.`);
@@ -230,13 +231,15 @@ async function pullBusiness(slug) {
230
231
  workspaceId = match.workspace_id;
231
232
  businessName = match.name;
232
233
  resolvedSlug = match.slug;
234
+ localSlug = businessMatchesSlug(match, slug) ? slug : match.slug;
233
235
 
234
236
  // Update local cache
235
237
  businesses[slug] = {
236
238
  business_id: businessId,
237
239
  workspace_id: workspaceId,
238
240
  name: businessName,
239
- slug: match.slug,
241
+ slug: localSlug,
242
+ canonical_slug: match.slug,
240
243
  added_at: new Date().toISOString(),
241
244
  };
242
245
  const { saveBusinesses } = require('./business');
@@ -697,7 +700,8 @@ async function pullBusiness(slug) {
697
700
  const atrisDir = path.join(outputDir, '.atris');
698
701
  fs.mkdirSync(atrisDir, { recursive: true });
699
702
  fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
700
- slug: resolvedSlug || slug,
703
+ slug: localSlug,
704
+ canonical_slug: resolvedSlug || slug,
701
705
  business_id: businessId,
702
706
  workspace_id: workspaceId,
703
707
  name: businessName,
package/commands/push.js CHANGED
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { loadCredentials } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
- const { loadBusinesses, saveBusinesses } = require('./business');
6
+ const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
7
7
  const { loadManifest, saveManifest, buildManifest, computeLocalHashes } = require('../lib/manifest');
8
8
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
9
9
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
@@ -92,7 +92,7 @@ async function pushAtris() {
92
92
  const businesses = loadBusinesses();
93
93
  const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
94
94
  if (listResult.ok) {
95
- const match = (listResult.data || []).find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
95
+ const match = (listResult.data || []).find(b => businessMatchesSlug(b, slug, { includeName: true }));
96
96
  if (!match) { console.error(`Business "${slug}" not found.`); process.exit(1); }
97
97
  businessId = match.id;
98
98
  workspaceId = match.workspace_id;
@@ -0,0 +1,217 @@
1
+ // `atris task` — SQLite-backed task plane. TODO.md stays the human-readable
2
+ // board; this gives agents atomic claims and a compact sync row.
3
+
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
11
+ || process.env.USER
12
+ || os.userInfo().username
13
+ || 'unknown';
14
+
15
+ let taskDbModule = null;
16
+
17
+ function getTaskDb() {
18
+ if (taskDbModule) return taskDbModule;
19
+ try {
20
+ taskDbModule = require('../lib/task-db');
21
+ return taskDbModule;
22
+ } catch (e) {
23
+ const message = String(e && (e.message || e));
24
+ const missingSqlite = e && (
25
+ e.code === 'ERR_UNKNOWN_BUILTIN_MODULE'
26
+ || /node:sqlite|No such built-in module/i.test(message)
27
+ );
28
+ if (missingSqlite) {
29
+ console.error('atris task requires Node.js 22+ because it uses built-in node:sqlite.');
30
+ console.error('Use the markdown TODO.md flow on older Node versions.');
31
+ process.exit(1);
32
+ }
33
+ throw e;
34
+ }
35
+ }
36
+
37
+ function help() {
38
+ console.log(`
39
+ atris task — local agent task plane (SQLite, gitignored)
40
+
41
+ atris task add "<title>" [--tag <tag>] Create a task
42
+ atris task list [--all] [--status <s>] List tasks (default: this workspace)
43
+ atris task claim <id> [--as <owner>] Atomic claim
44
+ atris task done <id> [--failed] Mark complete (or failed)
45
+ atris task import <file> One-shot import from TODO.md
46
+ atris task where Print db path + workspace scope
47
+ atris task help This help
48
+
49
+ Env:
50
+ ATRIS_TASKS_DB Override db path (default ~/.atris/tasks.db)
51
+ ATRIS_AGENT_ID Owner id for claim/done (default: $USER)
52
+ `.trim());
53
+ }
54
+
55
+ function flag(args, name) {
56
+ const i = args.indexOf(name);
57
+ if (i === -1) return null;
58
+ return args[i + 1] || true;
59
+ }
60
+
61
+ function hasFlag(args, name) {
62
+ return args.indexOf(name) !== -1;
63
+ }
64
+
65
+ function positional(args) {
66
+ return args.filter((a, i) => {
67
+ if (a.startsWith('--')) return false;
68
+ if (i > 0 && args[i - 1].startsWith('--')) return false;
69
+ return true;
70
+ });
71
+ }
72
+
73
+ function cmdAdd(args) {
74
+ const pos = positional(args);
75
+ const title = pos.join(' ').trim();
76
+ if (!title) {
77
+ console.error('atris task add: title required');
78
+ process.exit(2);
79
+ }
80
+ const tag = flag(args, '--tag');
81
+ const taskDb = getTaskDb();
82
+ const db = taskDb.open();
83
+ const ws = taskDb.workspaceRoot();
84
+ const result = taskDb.addTask(db, {
85
+ title,
86
+ tag: typeof tag === 'string' ? tag : null,
87
+ workspaceRoot: ws,
88
+ });
89
+ console.log(`${result.id}\t${title}`);
90
+ }
91
+
92
+ function cmdList(args) {
93
+ const all = hasFlag(args, '--all');
94
+ const status = flag(args, '--status');
95
+ const taskDb = getTaskDb();
96
+ const db = taskDb.open();
97
+ const rows = taskDb.listTasks(db, {
98
+ workspaceRoot: all ? null : taskDb.workspaceRoot(),
99
+ status: typeof status === 'string' ? status : null,
100
+ limit: 200,
101
+ });
102
+ if (rows.length === 0) {
103
+ console.log('(no tasks)');
104
+ return;
105
+ }
106
+ for (const r of rows) {
107
+ const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
108
+ const tag = r.tag ? ` #${r.tag}` : '';
109
+ console.log(`${r.status.padEnd(8)} ${r.id}${claim}${tag}\t${r.title}`);
110
+ }
111
+ }
112
+
113
+ function cmdClaim(args) {
114
+ const pos = positional(args);
115
+ const id = pos[0];
116
+ if (!id) {
117
+ console.error('atris task claim: id required');
118
+ process.exit(2);
119
+ }
120
+ const owner = flag(args, '--as') || DEFAULT_OWNER;
121
+ const taskDb = getTaskDb();
122
+ const db = taskDb.open();
123
+ const result = taskDb.claimTask(db, { id, claimedBy: String(owner) });
124
+ if (result.claimed) {
125
+ console.log(`claimed ${id} as ${owner}`);
126
+ } else {
127
+ console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ function cmdDone(args) {
133
+ const pos = positional(args);
134
+ const id = pos[0];
135
+ if (!id) {
136
+ console.error('atris task done: id required');
137
+ process.exit(2);
138
+ }
139
+ const failed = hasFlag(args, '--failed');
140
+ const taskDb = getTaskDb();
141
+ const db = taskDb.open();
142
+ const result = taskDb.doneTask(db, { id, status: failed ? 'failed' : 'done' });
143
+ if (result.updated) {
144
+ console.log(`${failed ? 'failed' : 'done'} ${id}`);
145
+ } else {
146
+ console.error(`done failed: ${id} not in open|claimed`);
147
+ process.exit(1);
148
+ }
149
+ }
150
+
151
+ function cmdImport(args) {
152
+ const pos = positional(args);
153
+ const target = pos[0] || 'atris/TODO.md';
154
+ const filePath = path.resolve(target);
155
+ if (!fs.existsSync(filePath)) {
156
+ console.error(`atris task import: file not found: ${filePath}`);
157
+ process.exit(2);
158
+ }
159
+ const { parseTodoFile } = require('../lib/todo-fallback');
160
+ const parsed = parseTodoFile(filePath);
161
+ const taskDb = getTaskDb();
162
+ const db = taskDb.open();
163
+ const ws = taskDb.workspaceRoot();
164
+ const all = [
165
+ ...parsed.backlog.map(t => ({ ...t, importStatus: 'open' })),
166
+ ...parsed.inProgress.map(t => ({ ...t, importStatus: 'claimed' })),
167
+ ];
168
+ let inserted = 0;
169
+ let skipped = 0;
170
+ for (const t of all) {
171
+ if (!t.title) continue;
172
+ const sk = taskDb.sourceKey(filePath, t.title);
173
+ const result = taskDb.addTask(db, {
174
+ title: t.title,
175
+ tag: t.tag || null,
176
+ workspaceRoot: ws,
177
+ sourceKey: sk,
178
+ status: t.importStatus,
179
+ claimedBy: t.claimed || null,
180
+ metadata: { todo_id: t.id, claimed: t.claimed, stage: t.stage, verify: t.verify },
181
+ });
182
+ if (result.inserted) inserted++; else skipped++;
183
+ }
184
+ console.log(`imported ${inserted} new, skipped ${skipped} (already imported), source=${filePath}`);
185
+ }
186
+
187
+ function cmdWhere() {
188
+ const taskDb = getTaskDb();
189
+ console.log(`db: ${taskDb.getDbPath()}`);
190
+ console.log(`workspace: ${taskDb.workspaceRoot()}`);
191
+ console.log(`owner: ${DEFAULT_OWNER}`);
192
+ }
193
+
194
+ async function run(args) {
195
+ const sub = (args && args[0]) || 'help';
196
+ const rest = (args || []).slice(1);
197
+ switch (sub) {
198
+ case 'add': return cmdAdd(rest);
199
+ case 'list': return cmdList(rest);
200
+ case 'ls': return cmdList(rest);
201
+ case 'claim': return cmdClaim(rest);
202
+ case 'done': return cmdDone(rest);
203
+ case 'fail': return cmdDone([...rest, '--failed']);
204
+ case 'import': return cmdImport(rest);
205
+ case 'where': return cmdWhere();
206
+ case 'help':
207
+ case '--help':
208
+ case '-h':
209
+ return help();
210
+ default:
211
+ console.error(`atris task: unknown subcommand "${sub}"`);
212
+ help();
213
+ process.exit(2);
214
+ }
215
+ }
216
+
217
+ module.exports = { run };