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.
- package/README.md +27 -3
- package/bin/atris.js +23 -7
- package/commands/business.js +393 -6
- package/commands/computer.js +181 -3
- package/commands/pull.js +8 -4
- package/commands/push.js +2 -2
- package/commands/task.js +217 -0
- package/lib/task-db.js +288 -0
- package/lib/todo-fallback.js +142 -0
- package/lib/todo.js +99 -184
- package/package.json +2 -2
- 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/README.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
It turns any repo into an AI workspace with shared context, a `plan -> do -> review` loop, daily logs, feature packs, and reusable skills.
|
|
6
6
|
|
|
7
|
+
Atris gives every owner persistent AI computers.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
Owner = User | Business
|
|
11
|
+
Owner has many Computers
|
|
12
|
+
Computer = workspace + files + tools + secrets + memory + agents + validation loop
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A computer can be personal or shared by a business. It can run a job like code, research, CRM, reporting, recruiting, event ops, support, or business ops.
|
|
16
|
+
|
|
7
17
|
## For Coding Agents
|
|
8
18
|
|
|
9
19
|
If you're an agent, run:
|
|
@@ -19,6 +29,8 @@ Then read the workspace's `atris/atris.md` and follow it exactly. `atris.md` is
|
|
|
19
29
|
## What Atris Gives You
|
|
20
30
|
|
|
21
31
|
- An AI workspace on top of any repo
|
|
32
|
+
- Persistent AI computers for scoped jobs
|
|
33
|
+
- A local computer card that makes each workspace inspectable
|
|
22
34
|
- A strict `plan -> do -> review` loop
|
|
23
35
|
- Daily logs, task tracking, feature packs, and project memory
|
|
24
36
|
- Skills, team members, integrations, and cloud sync when you need them
|
|
@@ -54,6 +66,7 @@ atris --version
|
|
|
54
66
|
```
|
|
55
67
|
|
|
56
68
|
Requires Node.js 18+.
|
|
69
|
+
`atris task` uses built-in SQLite and requires Node.js 22+.
|
|
57
70
|
|
|
58
71
|
If you want Atris cloud workspaces, businesses, or integrations, run `atris setup` after install.
|
|
59
72
|
|
|
@@ -73,9 +86,9 @@ Core loop: `plan` -> `do` -> `review`
|
|
|
73
86
|
|
|
74
87
|
Integrates with any agent.
|
|
75
88
|
|
|
76
|
-
## Business
|
|
89
|
+
## Business Owners
|
|
77
90
|
|
|
78
|
-
If you want a
|
|
91
|
+
If you want a shared owner for a company, lab, collective, community, artist, team, or project, use the business command instead of raw `atris init`.
|
|
79
92
|
|
|
80
93
|
```bash
|
|
81
94
|
atris business init "BLOND:ISH" --owner-email joel@blondish.world
|
|
@@ -84,10 +97,18 @@ atris business onboard --website https://blondish.world --contact "Joel Zimmerma
|
|
|
84
97
|
atris align --fix
|
|
85
98
|
```
|
|
86
99
|
|
|
87
|
-
That creates the
|
|
100
|
+
That creates the shared owner, creates its first/default computer, writes `.atris/business.json`, initializes `.atris/state/` for events and run history, and scaffolds the local `atris/` workspace under `~/arena/atris-business/<slug>/` with starter roles, a default recap template, and an initial task queue in `atris/TODO.md`.
|
|
88
101
|
|
|
89
102
|
If you do not have a neat source pack yet, `atris business onboard` is the easiest intake step: give it a website, a named human, a few notes, or run it in a folder with loose files. Atris turns that into raw intake, a starter brief, a first workflow, a safe next action, and a short operator brief.
|
|
90
103
|
|
|
104
|
+
Use the owner's language when you talk about it:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Your business runs on Atris.
|
|
108
|
+
Your lab runs on Atris.
|
|
109
|
+
Your collective runs on Atris.
|
|
110
|
+
```
|
|
111
|
+
|
|
91
112
|
You can also use bare input:
|
|
92
113
|
|
|
93
114
|
```bash
|
|
@@ -116,12 +137,14 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
116
137
|
| `atris autopilot` | Guided loop with approvals |
|
|
117
138
|
| `atris log` | Add inbox items to today's journal |
|
|
118
139
|
| `atris status` | Show active work and completions |
|
|
140
|
+
| `atris task` | Local agent task plane with atomic claims and TODO import |
|
|
119
141
|
| `atris learn` | Manage structured learnings |
|
|
120
142
|
| `atris ingest` | Stage raw evidence into `atris/context/` and compile into `atris/wiki/` |
|
|
121
143
|
| `atris loop` | Refresh wiki health, stale/orphan signals, and next ingest candidates |
|
|
122
144
|
| `atris wiki` | Full wiki namespace: ingest, query, lint, search, log, and loop |
|
|
123
145
|
| `atris receipt` | Save evidence from an agent run |
|
|
124
146
|
| `atris experiments` | Run small experiments and compare results |
|
|
147
|
+
| `atris computer card` | Show or write the local owner/computer card |
|
|
125
148
|
|
|
126
149
|
## Built-In Systems
|
|
127
150
|
|
|
@@ -131,6 +154,7 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
131
154
|
- `atris wiki --private` stores local-only sensitive notes under `.atris/presidio/`
|
|
132
155
|
- `atris loop` refreshes `atris/wiki/STATUS.md` and `atris/wiki/log.md`, flags stale/orphan pages, and suggests the next ingest
|
|
133
156
|
- `atris activate` loads the current wiki status so the next session starts with project memory, not just tasks
|
|
157
|
+
- `atris task` keeps a local SQLite task plane for agents while `atris/TODO.md` remains the readable project board
|
|
134
158
|
- `atris experiments` runs small test packs in `atris/experiments/`
|
|
135
159
|
- `atris pull` and `atris push` sync cloud workspaces and journals
|
|
136
160
|
|
package/bin/atris.js
CHANGED
|
@@ -204,19 +204,26 @@ function showHelp() {
|
|
|
204
204
|
console.log('');
|
|
205
205
|
console.log('Quick Start:');
|
|
206
206
|
console.log('');
|
|
207
|
-
console.log(' 1. atris
|
|
208
|
-
console.log(' 2. Describe what you want
|
|
209
|
-
console.log(' 3.
|
|
207
|
+
console.log(' 1. atris Open a persistent AI computer for this workspace');
|
|
208
|
+
console.log(' 2. Describe what you want run, built, researched, or validated');
|
|
209
|
+
console.log(' 3. Atris acts with context, memory, tools, and a review loop');
|
|
210
210
|
console.log('');
|
|
211
211
|
console.log('Common invocations:');
|
|
212
212
|
console.log(' atris init');
|
|
213
|
+
console.log(' atris computer');
|
|
214
|
+
console.log(' atris business init "My Company"');
|
|
213
215
|
console.log(' atris run');
|
|
214
216
|
console.log(' atris status');
|
|
215
217
|
console.log(' atris soul');
|
|
216
|
-
console.log(' atris fleet');
|
|
218
|
+
console.log(' atris fleet status');
|
|
217
219
|
console.log('');
|
|
218
220
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
219
221
|
console.log('');
|
|
222
|
+
console.log('Atris Computers:');
|
|
223
|
+
console.log(' Owner = User | Business');
|
|
224
|
+
console.log(' Owners have Computers: workspace + files + tools + secrets + memory + agents + validation');
|
|
225
|
+
console.log(' Types: code, research, CRM, reporting, recruiting, events, support, business ops');
|
|
226
|
+
console.log('');
|
|
220
227
|
console.log('Setup:');
|
|
221
228
|
console.log(' setup - Guided first-time setup (login, pick business, pull)');
|
|
222
229
|
console.log(' init - Initialize Atris in current project');
|
|
@@ -237,6 +244,7 @@ function showHelp() {
|
|
|
237
244
|
console.log(' search - Search journal history (atris search <keyword>)');
|
|
238
245
|
console.log(' clean - Housekeeping (stale tasks, archive journals, broken refs)');
|
|
239
246
|
console.log(' verify - Validate work is done (tests, MAP.md, changes)');
|
|
247
|
+
console.log(' task - Local agent task plane (atomic claims, TODO import)');
|
|
240
248
|
console.log(' release - Tag release, bump version, create GitHub release, draft /launch');
|
|
241
249
|
console.log(' learn - Project learnings (patterns, pitfalls, preferences)');
|
|
242
250
|
console.log(' ingest - Local-first wiki ingest into atris/wiki/');
|
|
@@ -272,7 +280,7 @@ function showHelp() {
|
|
|
272
280
|
console.log(' wake [business] - Resume workspace (agents restart)');
|
|
273
281
|
console.log('');
|
|
274
282
|
console.log('Business:');
|
|
275
|
-
console.log(' business init <name> -
|
|
283
|
+
console.log(' business init <name> - Create shared owner + first/default computer');
|
|
276
284
|
console.log(' business onboard - Onboard from sparse input (--name, --website, --contact)');
|
|
277
285
|
console.log(' business add <slug> - Connect a business');
|
|
278
286
|
console.log(' business list - Show connected businesses');
|
|
@@ -280,6 +288,7 @@ function showHelp() {
|
|
|
280
288
|
console.log(' business team [slug] - Show members, roles, and admin access');
|
|
281
289
|
console.log(' business health <slug> - Health report (members, workspace, issues)');
|
|
282
290
|
console.log(' business audit - One-line health summary of all businesses');
|
|
291
|
+
console.log(' business doctor - Catch stale cache, alias, and folder bindings');
|
|
283
292
|
console.log(' business create <name> - Cloud-only business record; add --workspace to also scaffold local');
|
|
284
293
|
console.log(' business connect <svc> - Wire a skill/integration');
|
|
285
294
|
console.log(' business notify <mode> - Set notification mode (digest/silent/push)');
|
|
@@ -290,9 +299,11 @@ function showHelp() {
|
|
|
290
299
|
console.log(' cr --all - Audit all backend services');
|
|
291
300
|
console.log('');
|
|
292
301
|
console.log('Cloud & agents:');
|
|
293
|
-
console.log(' computer -
|
|
302
|
+
console.log(' computer - Open a scoped AI computer (cloud/local, personal/business)');
|
|
294
303
|
console.log(' receipt - Save evidence from an agent run');
|
|
295
304
|
console.log(' console - Start/attach always-on coding console (tmux daemon)');
|
|
305
|
+
console.log(' soul - Show, snapshot, or fork workspace identity');
|
|
306
|
+
console.log(' fleet - Inspect local fleet status');
|
|
296
307
|
console.log(' agent - Select which Atris agent to use');
|
|
297
308
|
console.log(' chat - Chat with the selected Atris agent');
|
|
298
309
|
console.log(' login - Sign in or add another account');
|
|
@@ -434,7 +445,7 @@ const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('
|
|
|
434
445
|
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
435
446
|
'activate', '_activate', 'agent', 'chat', 'console', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
436
447
|
'clean', 'verify', 'search', 'skill', 'member', 'app', 'learn', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
|
|
437
|
-
'ingest', 'query', 'lint', 'loop',
|
|
448
|
+
'ingest', 'query', 'lint', 'loop', 'task',
|
|
438
449
|
'gmail', 'calendar', 'twitter', 'slack', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
439
450
|
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet'];
|
|
440
451
|
|
|
@@ -780,6 +791,11 @@ if (command === 'init') {
|
|
|
780
791
|
console.error(`✗ Error: ${error.message || error}`);
|
|
781
792
|
process.exit(1);
|
|
782
793
|
});
|
|
794
|
+
} else if (command === 'task') {
|
|
795
|
+
// SQLite-backed task plane. ~/.atris/tasks.db, gitignored, per-workspace.
|
|
796
|
+
Promise.resolve(require('../commands/task').run(process.argv.slice(3)))
|
|
797
|
+
.then(() => process.exit(0))
|
|
798
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
783
799
|
} else if (command === 'agent') {
|
|
784
800
|
agentAtris().then(() => process.exit(0)).catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
785
801
|
} else if (command === 'log') {
|
package/commands/business.js
CHANGED
|
@@ -33,10 +33,292 @@ function loadBusinesses() {
|
|
|
33
33
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function businessAliases(business) {
|
|
37
|
+
const aliases = business?.aliases || business?.config?.aliases || [];
|
|
38
|
+
return Array.isArray(aliases) ? aliases : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function businessMatchesSlug(business, slug, { includeName = false } = {}) {
|
|
42
|
+
if (!business || !slug) return false;
|
|
43
|
+
const wanted = String(slug).toLowerCase();
|
|
44
|
+
const canonical = String(business.slug || '').toLowerCase();
|
|
45
|
+
const aliases = businessAliases(business).map((alias) => String(alias).toLowerCase());
|
|
46
|
+
if (canonical === wanted || aliases.includes(wanted)) return true;
|
|
47
|
+
return includeName && String(business.name || '').toLowerCase() === wanted;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
function saveBusinesses(data) {
|
|
37
51
|
fs.writeFileSync(getBusinessConfigPath(), JSON.stringify(data, null, 2));
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
function buildBusinessCacheEntry(business, localSlug, existing = {}) {
|
|
55
|
+
const aliases = businessAliases(business);
|
|
56
|
+
const entry = {
|
|
57
|
+
business_id: business.id || business.business_id,
|
|
58
|
+
workspace_id: business.workspace_id,
|
|
59
|
+
name: business.name || localSlug,
|
|
60
|
+
slug: localSlug || business.slug,
|
|
61
|
+
added_at: existing.added_at || new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
if (business.slug && business.slug !== entry.slug) entry.canonical_slug = business.slug;
|
|
64
|
+
else if (business.slug) entry.canonical_slug = business.slug;
|
|
65
|
+
if (aliases.length > 0) entry.aliases = aliases;
|
|
66
|
+
return entry;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readBusinessFolderBindings(rootDir = path.join(os.homedir(), 'arena', 'atris-business')) {
|
|
70
|
+
const skip = new Set([
|
|
71
|
+
'.git', '.DS_Store', 'archive', 'archives', '_archive', 'bench', 'deals',
|
|
72
|
+
'node_modules', 'shelf', '_shelf', 'templates',
|
|
73
|
+
]);
|
|
74
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
75
|
+
|
|
76
|
+
return fs.readdirSync(rootDir, { withFileTypes: true })
|
|
77
|
+
.filter((entry) => !entry.name.startsWith('.') && !skip.has(entry.name))
|
|
78
|
+
.map((entry) => {
|
|
79
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
80
|
+
let isDirectory = entry.isDirectory();
|
|
81
|
+
const isSymlink = entry.isSymbolicLink();
|
|
82
|
+
let symlinkTarget = null;
|
|
83
|
+
if (isSymlink) {
|
|
84
|
+
try {
|
|
85
|
+
symlinkTarget = fs.readlinkSync(fullPath);
|
|
86
|
+
isDirectory = fs.statSync(fullPath).isDirectory();
|
|
87
|
+
} catch {
|
|
88
|
+
isDirectory = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!isDirectory) return null;
|
|
92
|
+
|
|
93
|
+
const businessJsonPath = path.join(fullPath, '.atris', 'business.json');
|
|
94
|
+
const atrisDir = path.join(fullPath, 'atris');
|
|
95
|
+
const binding = {
|
|
96
|
+
name: entry.name,
|
|
97
|
+
path: fullPath,
|
|
98
|
+
isSymlink,
|
|
99
|
+
symlinkTarget,
|
|
100
|
+
hasAtris: fs.existsSync(atrisDir) && fs.statSync(atrisDir).isDirectory(),
|
|
101
|
+
hasBusinessJson: fs.existsSync(businessJsonPath),
|
|
102
|
+
businessJsonPath,
|
|
103
|
+
meta: null,
|
|
104
|
+
error: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (binding.hasBusinessJson) {
|
|
108
|
+
try {
|
|
109
|
+
binding.meta = JSON.parse(fs.readFileSync(businessJsonPath, 'utf8'));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
binding.error = err.message || 'invalid JSON';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return binding;
|
|
115
|
+
})
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function analyzeBusinessDoctor({ cache = {}, cloudBusinesses = [], folderBindings = [] } = {}) {
|
|
120
|
+
const issues = [];
|
|
121
|
+
const cacheUpdates = {};
|
|
122
|
+
const activeById = new Map();
|
|
123
|
+
const activeBySlug = new Map();
|
|
124
|
+
const realFolderIds = new Map();
|
|
125
|
+
|
|
126
|
+
for (const business of cloudBusinesses || []) {
|
|
127
|
+
const id = business.id || business.business_id;
|
|
128
|
+
if (!id) continue;
|
|
129
|
+
activeById.set(id, business);
|
|
130
|
+
if (business.slug) {
|
|
131
|
+
const lower = String(business.slug).toLowerCase();
|
|
132
|
+
if (activeBySlug.has(lower)) {
|
|
133
|
+
issues.push({
|
|
134
|
+
level: 'fail',
|
|
135
|
+
code: 'duplicate-active-slug',
|
|
136
|
+
subject: business.slug,
|
|
137
|
+
message: `multiple active cloud businesses use slug ${business.slug}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
activeBySlug.set(lower, business);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const findActiveMatch = (key, entry = {}) => {
|
|
145
|
+
if (entry.business_id && activeById.has(entry.business_id)) return activeById.get(entry.business_id);
|
|
146
|
+
const candidates = [key, entry.slug, entry.canonical_slug, entry.name].filter(Boolean);
|
|
147
|
+
return cloudBusinesses.find((business) =>
|
|
148
|
+
candidates.some((candidate) => businessMatchesSlug(business, candidate, { includeName: true }))
|
|
149
|
+
) || null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const [key, entry] of Object.entries(cache || {})) {
|
|
153
|
+
const active = findActiveMatch(key, entry);
|
|
154
|
+
if (!active) {
|
|
155
|
+
issues.push({
|
|
156
|
+
level: 'fail',
|
|
157
|
+
code: 'stale-cache',
|
|
158
|
+
subject: key,
|
|
159
|
+
message: `${key} points at a deleted/inaccessible business (${entry.business_id || 'missing id'})`,
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (entry.business_id && entry.business_id !== active.id) {
|
|
165
|
+
issues.push({
|
|
166
|
+
level: 'fail',
|
|
167
|
+
code: 'stale-cache-repoint',
|
|
168
|
+
subject: key,
|
|
169
|
+
message: `${key} points at ${entry.business_id}; active cloud row is ${active.id}`,
|
|
170
|
+
fixable: true,
|
|
171
|
+
});
|
|
172
|
+
cacheUpdates[key] = buildBusinessCacheEntry(active, key, entry);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!businessMatchesSlug({ ...active, aliases: businessAliases(active) }, key, { includeName: true }) && key !== active.slug) {
|
|
176
|
+
issues.push({
|
|
177
|
+
level: 'warn',
|
|
178
|
+
code: 'cache-key-not-alias',
|
|
179
|
+
subject: key,
|
|
180
|
+
message: `${key} is cached but is not the canonical slug, alias, or name for ${active.slug}`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const business of cloudBusinesses || []) {
|
|
186
|
+
const canonicalKey = business.slug;
|
|
187
|
+
if (canonicalKey && (!cache[canonicalKey] || cache[canonicalKey].business_id !== business.id)) {
|
|
188
|
+
issues.push({
|
|
189
|
+
level: 'warn',
|
|
190
|
+
code: 'missing-canonical-cache',
|
|
191
|
+
subject: canonicalKey,
|
|
192
|
+
message: `${canonicalKey} is active in cloud but missing/stale in local cache`,
|
|
193
|
+
fixable: true,
|
|
194
|
+
});
|
|
195
|
+
cacheUpdates[canonicalKey] = buildBusinessCacheEntry(business, canonicalKey, cache[canonicalKey]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const alias of businessAliases(business)) {
|
|
199
|
+
if (!cache[alias] || cache[alias].business_id !== business.id) {
|
|
200
|
+
issues.push({
|
|
201
|
+
level: 'warn',
|
|
202
|
+
code: 'missing-alias-cache',
|
|
203
|
+
subject: alias,
|
|
204
|
+
message: `${alias} is an active alias for ${business.slug} but missing/stale in local cache`,
|
|
205
|
+
fixable: true,
|
|
206
|
+
});
|
|
207
|
+
cacheUpdates[alias] = buildBusinessCacheEntry(business, alias, cache[alias]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const binding of folderBindings || []) {
|
|
213
|
+
if (binding.error) {
|
|
214
|
+
issues.push({
|
|
215
|
+
level: 'fail',
|
|
216
|
+
code: 'invalid-business-json',
|
|
217
|
+
subject: binding.name,
|
|
218
|
+
message: `${binding.name}/.atris/business.json is invalid: ${binding.error}`,
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!binding.hasBusinessJson) {
|
|
224
|
+
if (binding.hasAtris && !binding.isSymlink) {
|
|
225
|
+
issues.push({
|
|
226
|
+
level: 'warn',
|
|
227
|
+
code: 'folder-unbound',
|
|
228
|
+
subject: binding.name,
|
|
229
|
+
message: `${binding.name} has atris/ but no .atris/business.json`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const meta = binding.meta || {};
|
|
236
|
+
const active = findActiveMatch(binding.name, {
|
|
237
|
+
business_id: meta.business_id,
|
|
238
|
+
slug: meta.slug,
|
|
239
|
+
canonical_slug: meta.canonical_slug,
|
|
240
|
+
name: meta.name,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!active) {
|
|
244
|
+
issues.push({
|
|
245
|
+
level: 'fail',
|
|
246
|
+
code: 'stale-folder-binding',
|
|
247
|
+
subject: binding.name,
|
|
248
|
+
message: `${binding.name} is bound to deleted/inaccessible business ${meta.business_id || meta.slug || 'unknown'}`,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (meta.business_id && meta.business_id !== active.id) {
|
|
254
|
+
issues.push({
|
|
255
|
+
level: 'fail',
|
|
256
|
+
code: 'folder-id-mismatch',
|
|
257
|
+
subject: binding.name,
|
|
258
|
+
message: `${binding.name} points at ${meta.business_id}; active cloud row is ${active.id}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (meta.slug && !businessMatchesSlug(active, meta.slug, { includeName: true })) {
|
|
263
|
+
issues.push({
|
|
264
|
+
level: 'fail',
|
|
265
|
+
code: 'folder-slug-mismatch',
|
|
266
|
+
subject: binding.name,
|
|
267
|
+
message: `${binding.name} uses slug ${meta.slug}, which is not ${active.slug} or an alias`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!businessMatchesSlug(active, binding.name, { includeName: false }) && !binding.isSymlink) {
|
|
272
|
+
issues.push({
|
|
273
|
+
level: 'warn',
|
|
274
|
+
code: 'folder-name-not-slug-or-alias',
|
|
275
|
+
subject: binding.name,
|
|
276
|
+
message: `${binding.name} is not a canonical slug or alias for ${active.slug}`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!binding.isSymlink) {
|
|
281
|
+
const existing = realFolderIds.get(active.id) || [];
|
|
282
|
+
existing.push(binding.name);
|
|
283
|
+
realFolderIds.set(active.id, existing);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const cacheKey = meta.slug || binding.name;
|
|
287
|
+
if (!cache[cacheKey] || cache[cacheKey].business_id !== active.id) {
|
|
288
|
+
issues.push({
|
|
289
|
+
level: 'warn',
|
|
290
|
+
code: 'folder-cache-missing',
|
|
291
|
+
subject: cacheKey,
|
|
292
|
+
message: `${binding.name} is bound locally but ${cacheKey} is missing/stale in local cache`,
|
|
293
|
+
fixable: true,
|
|
294
|
+
});
|
|
295
|
+
cacheUpdates[cacheKey] = buildBusinessCacheEntry(active, cacheKey, cache[cacheKey]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const [businessId, names] of realFolderIds.entries()) {
|
|
300
|
+
if (names.length > 1) {
|
|
301
|
+
issues.push({
|
|
302
|
+
level: 'fail',
|
|
303
|
+
code: 'duplicate-real-folders',
|
|
304
|
+
subject: businessId,
|
|
305
|
+
message: `business ${businessId} has multiple real folders: ${names.join(', ')}`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
issues,
|
|
312
|
+
cacheUpdates,
|
|
313
|
+
stats: {
|
|
314
|
+
cache_entries: Object.keys(cache || {}).length,
|
|
315
|
+
cloud_active: cloudBusinesses.length,
|
|
316
|
+
folders: folderBindings.length,
|
|
317
|
+
fixable_cache_entries: Object.keys(cacheUpdates).length,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
40
322
|
function parseCreateBusinessFlags(flags, cwd = process.cwd()) {
|
|
41
323
|
const options = {
|
|
42
324
|
description: '',
|
|
@@ -797,7 +1079,7 @@ async function findExistingBusinessBySlug(slug, token) {
|
|
|
797
1079
|
return { id: local[slug].business_id, name: local[slug].name, slug, source: 'local' };
|
|
798
1080
|
}
|
|
799
1081
|
for (const v of Object.values(local)) {
|
|
800
|
-
if (v
|
|
1082
|
+
if (businessMatchesSlug(v, slug)) {
|
|
801
1083
|
return { id: v.business_id, name: v.name, slug, source: 'local' };
|
|
802
1084
|
}
|
|
803
1085
|
}
|
|
@@ -815,7 +1097,7 @@ async function findExistingBusinessBySlug(slug, token) {
|
|
|
815
1097
|
|
|
816
1098
|
const list = await apiRequestJson('/business/', { method: 'GET', token });
|
|
817
1099
|
if (list.ok && Array.isArray(list.data)) {
|
|
818
|
-
const match = list.data.find(b => b
|
|
1100
|
+
const match = list.data.find(b => businessMatchesSlug(b, slug));
|
|
819
1101
|
if (match) return { id: match.id, name: match.name, slug: match.slug, source: 'cloud' };
|
|
820
1102
|
}
|
|
821
1103
|
|
|
@@ -848,7 +1130,7 @@ async function addBusiness(slug) {
|
|
|
848
1130
|
// Try listing all and matching
|
|
849
1131
|
const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
|
|
850
1132
|
if (listResult.ok && Array.isArray(listResult.data)) {
|
|
851
|
-
const match = listResult.data.find(b => b
|
|
1133
|
+
const match = listResult.data.find(b => businessMatchesSlug(b, slug, { includeName: true }));
|
|
852
1134
|
if (match) {
|
|
853
1135
|
const businesses = loadBusinesses();
|
|
854
1136
|
businesses[slug] = {
|
|
@@ -1099,7 +1381,7 @@ async function resolveSlug(slug, creds) {
|
|
|
1099
1381
|
// Fallback: list all and match
|
|
1100
1382
|
const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
|
|
1101
1383
|
if (listResult.ok && Array.isArray(listResult.data)) {
|
|
1102
|
-
const match = listResult.data.find(b => b
|
|
1384
|
+
const match = listResult.data.find(b => businessMatchesSlug(b, slug, { includeName: true }));
|
|
1103
1385
|
if (match) {
|
|
1104
1386
|
return { business_id: match.id, workspace_id: match.workspace_id, name: match.name, slug: match.slug };
|
|
1105
1387
|
}
|
|
@@ -1313,6 +1595,103 @@ async function businessAudit() {
|
|
|
1313
1595
|
console.log('');
|
|
1314
1596
|
}
|
|
1315
1597
|
|
|
1598
|
+
function parseBusinessDoctorOptions(args = []) {
|
|
1599
|
+
const options = {
|
|
1600
|
+
fix: args.includes('--fix'),
|
|
1601
|
+
json: args.includes('--json'),
|
|
1602
|
+
root: path.join(os.homedir(), 'arena', 'atris-business'),
|
|
1603
|
+
};
|
|
1604
|
+
const rootIdx = args.indexOf('--root');
|
|
1605
|
+
if (rootIdx !== -1 && args[rootIdx + 1]) {
|
|
1606
|
+
options.root = path.resolve(args[rootIdx + 1]);
|
|
1607
|
+
}
|
|
1608
|
+
return options;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function printBusinessDoctorHelp() {
|
|
1612
|
+
console.log('Usage: atris business doctor [--fix] [--root <dir>] [--json]');
|
|
1613
|
+
console.log('');
|
|
1614
|
+
console.log('Checks cloud-active businesses against:');
|
|
1615
|
+
console.log(' - ~/.atris/businesses.json');
|
|
1616
|
+
console.log(' - ~/arena/atris-business/*/.atris/business.json');
|
|
1617
|
+
console.log(' - canonical slug + alias bindings');
|
|
1618
|
+
console.log('');
|
|
1619
|
+
console.log('--fix rewrites only safe local cache entries. It does not rename folders or touch cloud data.');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
async function businessDoctor(...args) {
|
|
1623
|
+
if (args.some(isHelpToken)) {
|
|
1624
|
+
printBusinessDoctorHelp();
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const options = parseBusinessDoctorOptions(args);
|
|
1629
|
+
const creds = loadCredentials();
|
|
1630
|
+
if (!creds || !creds.token) {
|
|
1631
|
+
console.error('Not logged in. Run: atris login');
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
|
|
1636
|
+
if (!listResult.ok || !Array.isArray(listResult.data)) {
|
|
1637
|
+
console.error(`Failed to fetch businesses: ${listResult.errorMessage || listResult.error || listResult.status || 'unknown error'}`);
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
let cache = loadBusinesses();
|
|
1642
|
+
const folderBindings = readBusinessFolderBindings(options.root);
|
|
1643
|
+
let analysis = analyzeBusinessDoctor({
|
|
1644
|
+
cache,
|
|
1645
|
+
cloudBusinesses: listResult.data,
|
|
1646
|
+
folderBindings,
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
const cacheUpdateKeys = Object.keys(analysis.cacheUpdates);
|
|
1650
|
+
let fixed = [];
|
|
1651
|
+
if (options.fix && cacheUpdateKeys.length > 0) {
|
|
1652
|
+
cache = { ...cache, ...analysis.cacheUpdates };
|
|
1653
|
+
saveBusinesses(cache);
|
|
1654
|
+
fixed = cacheUpdateKeys;
|
|
1655
|
+
analysis = analyzeBusinessDoctor({
|
|
1656
|
+
cache,
|
|
1657
|
+
cloudBusinesses: listResult.data,
|
|
1658
|
+
folderBindings,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (options.json) {
|
|
1663
|
+
console.log(JSON.stringify({
|
|
1664
|
+
root: options.root,
|
|
1665
|
+
stats: analysis.stats,
|
|
1666
|
+
fixed,
|
|
1667
|
+
issues: analysis.issues,
|
|
1668
|
+
}, null, 2));
|
|
1669
|
+
} else {
|
|
1670
|
+
console.log('');
|
|
1671
|
+
console.log('Business Doctor');
|
|
1672
|
+
console.log('---------------');
|
|
1673
|
+
console.log(`cloud active: ${analysis.stats.cloud_active}`);
|
|
1674
|
+
console.log(`cache entries: ${analysis.stats.cache_entries}`);
|
|
1675
|
+
console.log(`folders scanned: ${analysis.stats.folders}`);
|
|
1676
|
+
if (fixed.length > 0) console.log(`fixed cache entries: ${fixed.join(', ')}`);
|
|
1677
|
+
console.log('');
|
|
1678
|
+
|
|
1679
|
+
if (analysis.issues.length === 0) {
|
|
1680
|
+
console.log('OK no business binding drift found.');
|
|
1681
|
+
} else {
|
|
1682
|
+
for (const issue of analysis.issues) {
|
|
1683
|
+
const label = issue.level === 'fail' ? 'FAIL' : 'WARN';
|
|
1684
|
+
const fixHint = issue.fixable ? ' (run with --fix)' : '';
|
|
1685
|
+
console.log(`${label} ${issue.code}: ${issue.message}${fixHint}`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
console.log('');
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const failures = analysis.issues.filter((issue) => issue.level === 'fail');
|
|
1692
|
+
if (failures.length > 0) process.exitCode = 1;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1316
1695
|
async function createBusinessInternal(name, flags = [], mode = 'auto') {
|
|
1317
1696
|
if (!name || isHelpToken(name) || String(name).startsWith('-')) {
|
|
1318
1697
|
console.error('Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]');
|
|
@@ -1778,7 +2157,7 @@ async function deployBusiness(slug) {
|
|
|
1778
2157
|
// Try to find by slug in cloud
|
|
1779
2158
|
const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
|
|
1780
2159
|
if (listResult.ok && Array.isArray(listResult.data)) {
|
|
1781
|
-
const match = listResult.data.find(b => b
|
|
2160
|
+
const match = listResult.data.find(b => businessMatchesSlug(b, slug));
|
|
1782
2161
|
if (match) {
|
|
1783
2162
|
bizConfig = { business_id: match.id, workspace_id: match.workspace_id, name: match.name, slug: match.slug };
|
|
1784
2163
|
businesses[slug] = { ...bizConfig, added_at: new Date().toISOString() };
|
|
@@ -1933,6 +2312,7 @@ function printBusinessHelp() {
|
|
|
1933
2312
|
console.log(' status <slug> Quick status check');
|
|
1934
2313
|
console.log(' health [slug] Full health dashboard');
|
|
1935
2314
|
console.log(' audit Audit all businesses');
|
|
2315
|
+
console.log(' doctor [--fix] Find stale business cache, alias, and folder bindings');
|
|
1936
2316
|
console.log(' connect <service> Connect a skill/integration');
|
|
1937
2317
|
console.log(' notify <mode> Set notification mode (digest/silent/push)');
|
|
1938
2318
|
console.log(' deploy <slug> Push local business to cloud');
|
|
@@ -1951,7 +2331,7 @@ async function businessCommand(subcommand, ...args) {
|
|
|
1951
2331
|
printBusinessHelp();
|
|
1952
2332
|
return;
|
|
1953
2333
|
}
|
|
1954
|
-
if (args.length > 0 && isHelpToken(args[0])) {
|
|
2334
|
+
if (args.length > 0 && isHelpToken(args[0]) && subcommand !== 'doctor') {
|
|
1955
2335
|
printBusinessHelp();
|
|
1956
2336
|
return;
|
|
1957
2337
|
}
|
|
@@ -2001,6 +2381,9 @@ async function businessCommand(subcommand, ...args) {
|
|
|
2001
2381
|
case 'audit':
|
|
2002
2382
|
await businessAudit();
|
|
2003
2383
|
break;
|
|
2384
|
+
case 'doctor':
|
|
2385
|
+
await businessDoctor(...args);
|
|
2386
|
+
break;
|
|
2004
2387
|
case 'connect':
|
|
2005
2388
|
await connectService(args[0], ...args.slice(1));
|
|
2006
2389
|
break;
|
|
@@ -2036,10 +2419,14 @@ module.exports = {
|
|
|
2036
2419
|
businessCommand,
|
|
2037
2420
|
businessHealth,
|
|
2038
2421
|
businessAudit,
|
|
2422
|
+
businessDoctor,
|
|
2039
2423
|
businessTeam,
|
|
2040
2424
|
loadBusinesses,
|
|
2041
2425
|
saveBusinesses,
|
|
2042
2426
|
getBusinessConfigPath,
|
|
2427
|
+
businessMatchesSlug,
|
|
2428
|
+
analyzeBusinessDoctor,
|
|
2429
|
+
readBusinessFolderBindings,
|
|
2043
2430
|
createCanonicalBusinessWorkspace,
|
|
2044
2431
|
initBusinessWorkspace,
|
|
2045
2432
|
onboardBusiness,
|