atris 2.6.2 → 3.0.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 (54) hide show
  1. package/README.md +124 -34
  2. package/atris/CLAUDE.md +5 -1
  3. package/atris/atris.md +4 -0
  4. package/atris/features/README.md +24 -0
  5. package/atris/skills/autopilot/SKILL.md +74 -75
  6. package/atris/skills/endgame/SKILL.md +179 -0
  7. package/atris/skills/flow/SKILL.md +121 -0
  8. package/atris/skills/improve/SKILL.md +84 -0
  9. package/atris/skills/loop/SKILL.md +72 -0
  10. package/atris/skills/wiki/SKILL.md +61 -0
  11. package/atris/team/executor/MEMBER.md +10 -4
  12. package/atris/team/navigator/MEMBER.md +2 -0
  13. package/atris/team/validator/MEMBER.md +8 -5
  14. package/atris.md +33 -0
  15. package/bin/atris.js +210 -41
  16. package/commands/activate.js +28 -2
  17. package/commands/align.js +720 -0
  18. package/commands/auth.js +75 -2
  19. package/commands/autopilot.js +1213 -270
  20. package/commands/browse.js +100 -0
  21. package/commands/business.js +785 -12
  22. package/commands/clean.js +107 -2
  23. package/commands/computer.js +429 -0
  24. package/commands/context-sync.js +78 -8
  25. package/commands/experiments.js +351 -0
  26. package/commands/feedback.js +150 -0
  27. package/commands/fleet.js +395 -0
  28. package/commands/fork.js +127 -0
  29. package/commands/init.js +50 -1
  30. package/commands/learn.js +407 -0
  31. package/commands/lifecycle.js +94 -0
  32. package/commands/loop.js +114 -0
  33. package/commands/publish.js +129 -0
  34. package/commands/pull.js +434 -48
  35. package/commands/push.js +312 -164
  36. package/commands/review.js +149 -0
  37. package/commands/run.js +76 -43
  38. package/commands/serve.js +360 -0
  39. package/commands/setup.js +1 -1
  40. package/commands/soul.js +381 -0
  41. package/commands/status.js +119 -1
  42. package/commands/sync.js +147 -1
  43. package/commands/terminal.js +201 -0
  44. package/commands/wiki.js +376 -0
  45. package/commands/workflow.js +191 -74
  46. package/commands/workspace-clean.js +3 -3
  47. package/lib/endstate.js +259 -0
  48. package/lib/learnings.js +235 -0
  49. package/lib/manifest.js +1 -0
  50. package/lib/todo.js +9 -5
  51. package/lib/wiki.js +578 -0
  52. package/package.json +2 -2
  53. package/utils/api.js +48 -36
  54. package/utils/auth.js +1 -0
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Atris Fleet — Manage the agent swarm via Swarlo
3
+ *
4
+ * Commands:
5
+ * atris fleet — Show fleet status
6
+ * atris fleet status — Same as above
7
+ * atris fleet post <message> — Post to the board
8
+ * atris fleet task <prompt> — Post a task for agents to claim
9
+ * atris fleet claim <task_key> — Claim a task
10
+ * atris fleet done <task_key> — Report task complete
11
+ * atris fleet members — List members
12
+ * atris fleet prune — Remove stale members
13
+ * atris fleet join — Register this session
14
+ */
15
+
16
+ const http = require('http');
17
+ const https = require('https');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+ const crypto = require('crypto');
22
+
23
+ // ── Config ──────────────────────────────────────────────
24
+
25
+ const DEFAULT_HUB_URL = 'http://localhost:8090';
26
+ const DEFAULT_HUB_ID = 'atris';
27
+
28
+ function loadConfig() {
29
+ const paths = [
30
+ path.join(os.homedir(), '.swarlo', 'config.json'),
31
+ path.join(process.cwd(), '.swarlo', 'config.json'),
32
+ ];
33
+ for (const p of paths) {
34
+ try {
35
+ const raw = fs.readFileSync(p, 'utf8');
36
+ return JSON.parse(raw);
37
+ } catch {}
38
+ }
39
+ return {};
40
+ }
41
+
42
+ function getHubUrl() {
43
+ const cfg = loadConfig();
44
+ return cfg.server || process.env.SWARLO_SERVER || DEFAULT_HUB_URL;
45
+ }
46
+
47
+ function getHubId() {
48
+ const cfg = loadConfig();
49
+ return cfg.hub || process.env.SWARLO_HUB || DEFAULT_HUB_ID;
50
+ }
51
+
52
+ // ── HTTP helpers ────────────────────────────────────────
53
+
54
+ function request(method, urlStr, body, headers = {}) {
55
+ return new Promise((resolve, reject) => {
56
+ const url = new URL(urlStr);
57
+ const mod = url.protocol === 'https:' ? https : http;
58
+ const opts = {
59
+ hostname: url.hostname,
60
+ port: url.port,
61
+ path: url.pathname + url.search,
62
+ method,
63
+ headers: { 'Content-Type': 'application/json', ...headers },
64
+ };
65
+ const req = mod.request(opts, (res) => {
66
+ let data = '';
67
+ res.on('data', (chunk) => (data += chunk));
68
+ res.on('end', () => {
69
+ try {
70
+ resolve({ status: res.statusCode, data: JSON.parse(data) });
71
+ } catch {
72
+ resolve({ status: res.statusCode, data: data });
73
+ }
74
+ });
75
+ });
76
+ req.on('error', reject);
77
+ if (body) req.write(JSON.stringify(body));
78
+ req.end();
79
+ });
80
+ }
81
+
82
+ // ── State file for API key persistence ──────────────────
83
+
84
+ const STATE_FILE = path.join(os.homedir(), '.swarlo', 'fleet-state.json');
85
+
86
+ function loadState() {
87
+ try {
88
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ function saveState(state) {
95
+ const dir = path.dirname(STATE_FILE);
96
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
97
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
98
+ }
99
+
100
+ // ── Auto-register ───────────────────────────────────────
101
+
102
+ async function ensureRegistered() {
103
+ const state = loadState();
104
+ const hubUrl = getHubUrl();
105
+ const hubId = getHubId();
106
+
107
+ // Check if hub is up
108
+ try {
109
+ await request('GET', `${hubUrl}/api/health`);
110
+ } catch {
111
+ console.error('✗ Swarlo hub not running at', hubUrl);
112
+ console.error(' Start it: swarlo serve --port 8090');
113
+ process.exit(1);
114
+ }
115
+
116
+ // Re-use existing key if valid
117
+ if (state.api_key && state.hub_id === hubId) {
118
+ try {
119
+ const check = await request('GET', `${hubUrl}/api/${hubId}/members`, null, {
120
+ Authorization: `Bearer ${state.api_key}`,
121
+ });
122
+ if (check.status === 200) return state;
123
+ } catch {}
124
+ }
125
+
126
+ // Register fresh
127
+ const memberId = `cli-${os.hostname().split('.')[0]}-${process.pid}`;
128
+ const memberName = `CLI (${os.userInfo().username})`;
129
+ const res = await request('POST', `${hubUrl}/api/register`, {
130
+ hub_id: hubId,
131
+ member_id: memberId,
132
+ member_name: memberName,
133
+ member_type: 'agent',
134
+ });
135
+
136
+ if (res.status >= 300) {
137
+ console.error('✗ Failed to register:', res.data);
138
+ process.exit(1);
139
+ }
140
+
141
+ const newState = {
142
+ api_key: res.data.api_key,
143
+ hub_id: hubId,
144
+ member_id: res.data.member_id,
145
+ hub_url: hubUrl,
146
+ };
147
+ saveState(newState);
148
+ return newState;
149
+ }
150
+
151
+ function authHeaders(state) {
152
+ return { Authorization: `Bearer ${state.api_key}` };
153
+ }
154
+
155
+ // ── Commands ────────────────────────────────────────────
156
+
157
+ async function fleetStatus(state) {
158
+ const hubUrl = getHubUrl();
159
+ const hubId = getHubId();
160
+ const h = authHeaders(state);
161
+
162
+ const [members, posts, claims] = await Promise.all([
163
+ request('GET', `${hubUrl}/api/${hubId}/members`, null, h),
164
+ request('GET', `${hubUrl}/api/${hubId}/channels/general/posts?limit=5`, null, h),
165
+ request('GET', `${hubUrl}/api/${hubId}/claims`, null, h),
166
+ ]);
167
+
168
+ const m = members.data;
169
+ const p = posts.data;
170
+ const c = claims.data;
171
+
172
+ console.log(`\n ⚡ Swarlo Fleet`);
173
+ console.log(` ─────────────────────────────────`);
174
+ console.log(` Hub: ${hubId} (${hubUrl})`);
175
+ console.log(` You: ${state.member_id}`);
176
+ console.log(` Members: ${m.count || 0}`);
177
+ if (m.members) {
178
+ for (const mem of m.members) {
179
+ const seen = mem.last_seen ? ` (seen ${mem.last_seen.slice(0, 16)})` : '';
180
+ console.log(` ${mem.member_type === 'human' ? '👤' : '🤖'} ${mem.member_name}${seen}`);
181
+ }
182
+ }
183
+ console.log(` Claims: ${c.count || 0} open`);
184
+ if (c.claims) {
185
+ for (const cl of c.claims) {
186
+ console.log(` [${cl.status}] ${cl.task_key || 'no-key'}: ${(cl.content || '').slice(0, 60)}`);
187
+ }
188
+ }
189
+ console.log(` Recent: ${p.posts?.length || 0} posts`);
190
+ if (p.posts) {
191
+ for (const post of p.posts.slice(0, 3)) {
192
+ console.log(` [${post.member_name}] ${(post.content || '').slice(0, 60)}`);
193
+ }
194
+ }
195
+ console.log();
196
+ }
197
+
198
+ async function fleetPost(state, message) {
199
+ const hubUrl = getHubUrl();
200
+ const hubId = getHubId();
201
+ const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
202
+ content: message,
203
+ kind: 'message',
204
+ }, authHeaders(state));
205
+ if (res.status < 300) {
206
+ console.log('✓ Posted to general');
207
+ } else {
208
+ console.error('✗ Failed:', res.data);
209
+ }
210
+ }
211
+
212
+ async function fleetTask(state, prompt) {
213
+ const hubUrl = getHubUrl();
214
+ const hubId = getHubId();
215
+ const taskKey = `task:${prompt.slice(0, 40).replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
216
+ const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
217
+ content: prompt,
218
+ kind: 'message',
219
+ task_key: taskKey,
220
+ }, authHeaders(state));
221
+ if (res.status < 300) {
222
+ console.log(`✓ Task posted: ${taskKey}`);
223
+ } else {
224
+ console.error('✗ Failed:', res.data);
225
+ }
226
+ }
227
+
228
+ async function fleetClaim(state, taskKey) {
229
+ const hubUrl = getHubUrl();
230
+ const hubId = getHubId();
231
+ const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
232
+ content: `Claiming: ${taskKey}`,
233
+ kind: 'claim',
234
+ task_key: taskKey,
235
+ }, authHeaders(state));
236
+ if (res.status < 300) {
237
+ console.log(`✓ Claimed: ${taskKey}`);
238
+ } else {
239
+ console.error('✗ Failed:', res.data);
240
+ }
241
+ }
242
+
243
+ async function fleetDone(state, taskKey, result = '') {
244
+ const hubUrl = getHubUrl();
245
+ const hubId = getHubId();
246
+ const res = await request('POST', `${hubUrl}/api/${hubId}/channels/general/posts`, {
247
+ content: result || `Done: ${taskKey}`,
248
+ kind: 'result',
249
+ task_key: taskKey,
250
+ }, authHeaders(state));
251
+ if (res.status < 300) {
252
+ console.log(`✓ Reported done: ${taskKey}`);
253
+ } else {
254
+ console.error('✗ Failed:', res.data);
255
+ }
256
+ }
257
+
258
+ async function fleetMembers(state) {
259
+ const hubUrl = getHubUrl();
260
+ const hubId = getHubId();
261
+ const res = await request('GET', `${hubUrl}/api/${hubId}/members`, null, authHeaders(state));
262
+ if (res.data?.members) {
263
+ console.log(`\n Members (${res.data.count}):`);
264
+ for (const m of res.data.members) {
265
+ const seen = m.last_seen ? m.last_seen.slice(0, 16) : 'never';
266
+ console.log(` ${m.member_type === 'human' ? '👤' : '🤖'} ${m.member_name} (${m.member_id}) — seen: ${seen}`);
267
+ }
268
+ console.log();
269
+ }
270
+ }
271
+
272
+ async function fleetPrune(state, minutes = 60) {
273
+ const hubUrl = getHubUrl();
274
+ const hubId = getHubId();
275
+ const res = await request('POST', `${hubUrl}/api/${hubId}/prune`, {
276
+ stale_minutes: minutes,
277
+ }, authHeaders(state));
278
+ if (res.data?.pruned) {
279
+ console.log(`✓ Pruned ${res.data.count} stale members: ${res.data.pruned.join(', ') || 'none'}`);
280
+ } else {
281
+ console.log('✓ No stale members to prune');
282
+ }
283
+ }
284
+
285
+ async function fleetWatch(state, intervalSec = 10) {
286
+ const hubUrl = getHubUrl();
287
+ const hubId = getHubId();
288
+ const h = authHeaders(state);
289
+ let lastPostId = null;
290
+
291
+ console.log(`\n ⚡ Watching Swarlo board (every ${intervalSec}s, Ctrl+C to stop)\n`);
292
+
293
+ const poll = async () => {
294
+ try {
295
+ const res = await request('GET', `${hubUrl}/api/${hubId}/channels/general/posts?limit=5`, null, h);
296
+ const posts = res.data?.posts || [];
297
+
298
+ if (posts.length > 0 && posts[0].post_id !== lastPostId) {
299
+ // Show new posts (reverse to show oldest first)
300
+ const newPosts = lastPostId
301
+ ? posts.filter(p => {
302
+ // Show posts newer than last seen
303
+ return !lastPostId || new Date(p.created_at) > new Date(posts.find(x => x.post_id === lastPostId)?.created_at || 0);
304
+ }).reverse()
305
+ : [posts[0]]; // First poll, just show latest
306
+
307
+ for (const p of newPosts) {
308
+ const time = new Date(p.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
309
+ const kind = p.kind !== 'message' ? ` [${p.kind}]` : '';
310
+ console.log(` ${time} ${p.member_name}${kind}: ${(p.content || '').slice(0, 100)}`);
311
+ }
312
+ lastPostId = posts[0].post_id;
313
+ }
314
+ } catch {}
315
+ };
316
+
317
+ await poll();
318
+ const interval = setInterval(poll, intervalSec * 1000);
319
+
320
+ // Keep alive until Ctrl+C
321
+ await new Promise((resolve) => {
322
+ process.on('SIGINT', () => {
323
+ clearInterval(interval);
324
+ console.log('\n Stopped watching.\n');
325
+ resolve();
326
+ });
327
+ });
328
+ }
329
+
330
+ // ── Main ────────────────────────────────────────────────
331
+
332
+ async function fleet(args = []) {
333
+ const subcommand = (args[0] || 'status').toLowerCase();
334
+ const rest = args.slice(1).join(' ');
335
+
336
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
337
+ console.log('');
338
+ console.log(' atris fleet — coordinate agent swarm via Swarlo');
339
+ console.log('');
340
+ console.log(' fleet show board status');
341
+ console.log(' fleet post <msg> post message to board');
342
+ console.log(' fleet task <prompt> create a task for agents');
343
+ console.log(' fleet claim <key> claim a task');
344
+ console.log(' fleet done <key> report task complete');
345
+ console.log(' fleet members who is online');
346
+ console.log(' fleet prune [min] remove stale members (default 60m)');
347
+ console.log(' fleet watch [sec] live-tail the board (default 10s)');
348
+ console.log('');
349
+ return;
350
+ }
351
+
352
+ const state = await ensureRegistered();
353
+
354
+ switch (subcommand) {
355
+ case 'status':
356
+ case 'st':
357
+ await fleetStatus(state);
358
+ break;
359
+ case 'post':
360
+ case 'say':
361
+ if (!rest) { console.error('Usage: atris fleet post <message>'); return; }
362
+ await fleetPost(state, rest);
363
+ break;
364
+ case 'task':
365
+ if (!rest) { console.error('Usage: atris fleet task <prompt>'); return; }
366
+ await fleetTask(state, rest);
367
+ break;
368
+ case 'claim':
369
+ if (!rest) { console.error('Usage: atris fleet claim <task_key>'); return; }
370
+ await fleetClaim(state, rest);
371
+ break;
372
+ case 'done':
373
+ case 'report':
374
+ if (!rest) { console.error('Usage: atris fleet done <task_key> [result]'); return; }
375
+ const [key, ...resultParts] = rest.split(' ');
376
+ await fleetDone(state, key, resultParts.join(' '));
377
+ break;
378
+ case 'members':
379
+ case 'who':
380
+ await fleetMembers(state);
381
+ break;
382
+ case 'prune':
383
+ await fleetPrune(state, parseInt(rest) || 60);
384
+ break;
385
+ case 'watch':
386
+ case 'tail':
387
+ await fleetWatch(state, parseInt(rest) || 10);
388
+ break;
389
+ default:
390
+ // If no subcommand match, treat the whole thing as status
391
+ await fleetStatus(state);
392
+ }
393
+ }
394
+
395
+ module.exports = { fleet };
@@ -0,0 +1,127 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { loadCredentials } = require('../utils/auth');
5
+ const { apiRequestJson } = require('../utils/api');
6
+
7
+ async function forkAtris() {
8
+ const template = process.argv[3];
9
+ const targetArg = process.argv[4];
10
+
11
+ if (!template || template === '--help') {
12
+ console.log('Usage: atris fork <template> [target-dir]');
13
+ console.log('');
14
+ console.log(' atris fork music-artist Fork the music-artist template');
15
+ console.log(' atris fork event-promoter myband Fork into ./myband/');
16
+ console.log(' atris fork pallet Fork from a business slug');
17
+ console.log('');
18
+ console.log('Templates can be a name, business slug, or URL.');
19
+ process.exit(0);
20
+ }
21
+
22
+ const creds = loadCredentials();
23
+ if (!creds || !creds.token) {
24
+ console.error('Not logged in. Run: atris login');
25
+ process.exit(1);
26
+ }
27
+
28
+ // Resolve target directory
29
+ const targetName = targetArg && !targetArg.startsWith('-') ? targetArg : template;
30
+ const targetDir = path.resolve(process.cwd(), targetName);
31
+
32
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
33
+ console.error(`Directory "${targetName}" already exists and is not empty.`);
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log('');
38
+ console.log(`Forking template "${template}"...`);
39
+
40
+ // Try API first
41
+ let files = null;
42
+ const downloadResult = await apiRequestJson(
43
+ `/workspace/templates/${encodeURIComponent(template)}/download`,
44
+ { method: 'GET', token: creds.token }
45
+ );
46
+
47
+ if (downloadResult.ok && downloadResult.data && downloadResult.data.files) {
48
+ files = downloadResult.data.files;
49
+ } else {
50
+ // Fall back to local template at ~/.atris/templates/{template}/
51
+ const localTemplatePath = path.join(os.homedir(), '.atris', 'templates', template);
52
+ if (fs.existsSync(localTemplatePath)) {
53
+ console.log(' API unavailable, using local template...');
54
+ files = readLocalTemplate(localTemplatePath, localTemplatePath);
55
+ } else {
56
+ const msg = downloadResult.errorMessage || downloadResult.error || `HTTP ${downloadResult.status}`;
57
+ console.error(`\n Template "${template}" not found. ${msg}`);
58
+ console.error(' Check available templates or provide a valid business slug.');
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ if (!files || files.length === 0) {
64
+ console.error(`\n Template "${template}" is empty.`);
65
+ process.exit(1);
66
+ }
67
+
68
+ // Create target directory and .atris metadata
69
+ const atrisDir = path.join(targetDir, '.atris');
70
+ fs.mkdirSync(atrisDir, { recursive: true });
71
+
72
+ // Write .atris/business.json pointing to template source
73
+ fs.writeFileSync(path.join(atrisDir, 'business.json'), JSON.stringify({
74
+ slug: targetName,
75
+ forked_from: template,
76
+ }, null, 2));
77
+
78
+ // Write .atris/fork.json with metadata
79
+ fs.writeFileSync(path.join(atrisDir, 'fork.json'), JSON.stringify({
80
+ forked_from: template,
81
+ forked_at: new Date().toISOString(),
82
+ version: '1.0.0',
83
+ }, null, 2));
84
+
85
+ // Write all template files to disk
86
+ let written = 0;
87
+ console.log('');
88
+ for (const file of files) {
89
+ if (!file.path || file.content === null || file.content === undefined) continue;
90
+ const relPath = file.path.replace(/^\//, '');
91
+ const filePath = path.join(targetDir, relPath);
92
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
93
+ fs.writeFileSync(filePath, file.content);
94
+ console.log(` + ${relPath}`);
95
+ written++;
96
+ }
97
+
98
+ // Summary
99
+ console.log('');
100
+ console.log(` ${written} file${written !== 1 ? 's' : ''} written to ${targetName}/`);
101
+ console.log('');
102
+ console.log(' Next steps:');
103
+ console.log(' 1. Customize your context files');
104
+ console.log(' 2. Run: atris push to go live');
105
+ console.log('');
106
+ }
107
+
108
+ function readLocalTemplate(baseDir, currentDir) {
109
+ const files = [];
110
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ const fullPath = path.join(currentDir, entry.name);
113
+ if (entry.isDirectory()) {
114
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
115
+ files.push(...readLocalTemplate(baseDir, fullPath));
116
+ } else {
117
+ try {
118
+ const content = fs.readFileSync(fullPath, 'utf8');
119
+ const relPath = '/' + path.relative(baseDir, fullPath);
120
+ files.push({ path: relPath, content });
121
+ } catch {}
122
+ }
123
+ }
124
+ return files;
125
+ }
126
+
127
+ module.exports = { forkAtris };
package/commands/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { ensureExperimentsFramework } = require('./experiments');
4
+ const { ensureWikiScaffold } = require('../lib/wiki');
4
5
 
5
6
  /**
6
7
  * Detect project context by scanning project structure
@@ -232,7 +233,48 @@ ${profile.hasCode ? `**Validation:** Run \`${profile.testCommand}\` to verify ch
232
233
  }
233
234
 
234
235
  function initAtris() {
235
- const targetDir = path.join(process.cwd(), 'atris');
236
+ // GUARD: Refuse nested init.
237
+ // Bug: running `atris init` inside an existing `atris/` folder creates
238
+ // `atris/atris/` nesting hell. Cloud doordash had this exact problem.
239
+ // Fix: detect three nesting conditions and refuse with a clear error.
240
+ const cwd = process.cwd();
241
+ const cwdBase = path.basename(cwd);
242
+ const force = process.argv.includes('--force');
243
+
244
+ if (cwdBase === 'atris' && !force) {
245
+ console.error('✗ Cannot run atris init inside an atris/ directory.');
246
+ console.error(' You appear to be inside the atris/ folder of an existing workspace.');
247
+ console.error(' Run init from the parent directory, or use --force to proceed anyway.');
248
+ process.exit(1);
249
+ }
250
+
251
+ // Check cwd itself for .atris/business.json — already a business workspace
252
+ const cwdBusinessJson = path.join(cwd, '.atris', 'business.json');
253
+ if (fs.existsSync(cwdBusinessJson) && !force) {
254
+ console.error('✗ This directory is already a business workspace (found .atris/business.json).');
255
+ console.error(' To update canonical files: atris update');
256
+ console.error(' To re-init anyway: atris init --force');
257
+ process.exit(1);
258
+ }
259
+
260
+ // Walk up to 6 parent dirs looking for an .atris/business.json — if found, we're inside a workspace
261
+ let walker = path.dirname(cwd);
262
+ for (let depth = 0; depth < 6; depth++) {
263
+ const businessJson = path.join(walker, '.atris', 'business.json');
264
+ if (fs.existsSync(businessJson)) {
265
+ if (!force) {
266
+ console.error(`✗ Cannot run atris init: parent directory ${walker} is already an atris workspace.`);
267
+ console.error(' Found .atris/business.json in a parent directory.');
268
+ console.error(' Run init from outside the workspace, or use --force to proceed anyway.');
269
+ process.exit(1);
270
+ }
271
+ }
272
+ const parent = path.dirname(walker);
273
+ if (parent === walker) break;
274
+ walker = parent;
275
+ }
276
+
277
+ const targetDir = path.join(cwd, 'atris');
236
278
  const teamDir = path.join(targetDir, 'team');
237
279
  const legacyAgentTeamDir = path.join(targetDir, 'agent_team');
238
280
  const sourceFile = path.join(__dirname, '..', 'atris.md');
@@ -304,6 +346,13 @@ function initAtris() {
304
346
  console.log('✓ Created PERSONA.md');
305
347
  }
306
348
 
349
+ const wikiDir = path.join(targetDir, 'wiki');
350
+ const wikiAlreadyExists = fs.existsSync(wikiDir);
351
+ ensureWikiScaffold(process.cwd());
352
+ if (!wikiAlreadyExists) {
353
+ console.log('✓ Created wiki/ scaffold');
354
+ }
355
+
307
356
  if (!fs.existsSync(mapFile)) {
308
357
  fs.writeFileSync(mapFile, '# MAP.md\n\n> Generated by your AI agent after reading atris.md\n\nRun your AI agent with atris.md to populate this file.\n');
309
358
  console.log('✓ Created MAP.md placeholder');