atris 3.12.1 → 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.
@@ -0,0 +1,115 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function readBusinessBinding(cwd = process.cwd()) {
5
+ const bindingPath = path.join(cwd, '.atris', 'business.json');
6
+ if (!fs.existsSync(bindingPath)) return null;
7
+ try {
8
+ return JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ function slugify(value) {
15
+ return String(value || 'business-workflow')
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '') || 'business-workflow';
19
+ }
20
+
21
+ function ensureDir(dir) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function writeJson(filePath, value) {
26
+ ensureDir(path.dirname(filePath));
27
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
28
+ }
29
+
30
+ function doctor(cwd = process.cwd()) {
31
+ const binding = readBusinessBinding(cwd);
32
+ console.log('Receipt check');
33
+ console.log(`business binding: ${binding ? `${binding.name || binding.slug || binding.business_id} ready` : 'missing'}`);
34
+ console.log(`receipt folder: ${fs.existsSync(path.join(cwd, '.atris', 'receipts')) ? 'ready' : 'missing'}`);
35
+ console.log('');
36
+ console.log('Next: atris receipt init business-workflow');
37
+ }
38
+
39
+ function init(taskSlug = 'business-workflow', cwd = process.cwd()) {
40
+ const binding = readBusinessBinding(cwd);
41
+ if (!binding) {
42
+ console.error('No business binding found. Run: atris business init <name> --here');
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ const slug = slugify(taskSlug);
48
+ const root = path.join(cwd, '.atris');
49
+ const taskPath = path.join(root, 'tasks', `${slug}.json`);
50
+ const receiptsDir = path.join(root, 'receipts');
51
+
52
+ ensureDir(receiptsDir);
53
+ fs.writeFileSync(path.join(receiptsDir, '.gitkeep'), '');
54
+ writeJson(taskPath, {
55
+ schema: 'atris.receipt_task.v1',
56
+ slug,
57
+ goal: 'Run one business-computer task and save what happened.',
58
+ workspace: {
59
+ business_id: binding.business_id || binding.id || null,
60
+ workspace_id: binding.workspace_id || null,
61
+ name: binding.name || null,
62
+ slug: binding.slug || null,
63
+ },
64
+ runtime: {
65
+ proof_command: 'atris computer proof',
66
+ replay_command: 'atris experiments replay endstate',
67
+ },
68
+ verify: [
69
+ 'atris computer proof',
70
+ 'atris experiments replay endstate',
71
+ ],
72
+ });
73
+
74
+ console.log(`Receipt task ready: ${slug}`);
75
+ console.log(`Task: ${path.relative(cwd, taskPath)}`);
76
+ console.log(`Receipts: ${path.relative(cwd, receiptsDir)}`);
77
+ }
78
+
79
+ function run(args = []) {
80
+ const dryRun = args.includes('--dry-run');
81
+ console.log('Receipt run');
82
+ console.log('1. atris computer proof');
83
+ console.log('2. atris experiments replay endstate');
84
+ if (dryRun) {
85
+ console.log('Dry run only; no receipts written.');
86
+ return;
87
+ }
88
+ console.log('Run those commands, then save the receipt under .atris/receipts/.');
89
+ }
90
+
91
+ function proofCommand(subcommand = 'doctor', ...args) {
92
+ switch (subcommand || 'doctor') {
93
+ case 'doctor':
94
+ return doctor();
95
+ case 'init':
96
+ return init(args[0] || 'business-workflow');
97
+ case 'proof':
98
+ return run(args);
99
+ case 'help':
100
+ case '--help':
101
+ case '-h':
102
+ console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
103
+ return;
104
+ case 'run':
105
+ return run(args);
106
+ default:
107
+ console.error(`Unknown receipt command: ${subcommand}`);
108
+ console.log('Usage: atris receipt [doctor|init <slug>|run --dry-run]');
109
+ process.exitCode = 1;
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ proofCommand,
115
+ };
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,14 +700,17 @@ 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,
704
708
  }, null, 2));
705
709
 
706
- // Wire skills → .claude/skills/ so they work as slash commands
707
- const skillsDir = path.join(outputDir, 'skills');
710
+ // Wire skills → .claude/skills/ so they work as slash commands.
711
+ // Source of truth is atris/skills/ (vendor-neutral, syncs to cloud).
712
+ // .claude/skills/ is a locally-generated adapter Claude Code reads from.
713
+ const skillsDir = path.join(outputDir, 'atris', 'skills');
708
714
  const claudeSkillsDir = path.join(outputDir, '.claude', 'skills');
709
715
 
710
716
  if (fs.existsSync(skillsDir)) {
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;
@@ -367,6 +367,12 @@ async function pushAtris() {
367
367
 
368
368
  if (!result.ok) {
369
369
  if (result.status === 403) {
370
+ const detail = result.errorMessage || result.error || (result.data && result.data.detail) || '';
371
+ if (detail && /plan required|business, max, or enterprise/i.test(detail)) {
372
+ console.error(`\n Access denied: ${detail}`);
373
+ await emit('access_denied', { error_detail: detail });
374
+ process.exit(1);
375
+ }
370
376
  // Permission denied — retry with only team/ and journal/ files
371
377
  const allowed = filesToPush.filter(f => f.path.startsWith('/team/') || f.path.startsWith('/journal/'));
372
378
  skipped = filesToPush.filter(f => !f.path.startsWith('/team/') && !f.path.startsWith('/journal/'));
@@ -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 };