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,381 @@
1
+ /**
2
+ * Atris Soul — The persona as a living artifact
3
+ *
4
+ * Every atris project has a soul: persona + policies + learnings + context.
5
+ * This command lets you see it, evolve it, fork it.
6
+ *
7
+ * atris soul — Show your project's soul state
8
+ * atris soul snapshot — Export full soul to JSON
9
+ * atris soul distill — Compress learnings into persona
10
+ * atris soul fork <target> — Copy soul to another project
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ function findAtrisDir() {
17
+ let dir = process.cwd();
18
+ while (dir !== path.dirname(dir)) {
19
+ if (fs.existsSync(path.join(dir, 'atris'))) return path.join(dir, 'atris');
20
+ dir = path.dirname(dir);
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function readFile(filePath) {
26
+ try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
27
+ }
28
+
29
+ function readJson(filePath) {
30
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
31
+ }
32
+
33
+ function countFiles(dir) {
34
+ try { return fs.readdirSync(dir, { recursive: true }).filter(f => !f.startsWith('.')).length; } catch { return 0; }
35
+ }
36
+
37
+ // ── Soul snapshot ──────────────────────────────────────
38
+
39
+ function snapshotSoul(atrisDir) {
40
+ const soul = {
41
+ timestamp: new Date().toISOString(),
42
+ project: path.basename(path.dirname(atrisDir)),
43
+ identity: {},
44
+ knowledge: {},
45
+ learned: {},
46
+ stats: {},
47
+ };
48
+
49
+ // Identity
50
+ const persona = readFile(path.join(atrisDir, 'PERSONA.md'));
51
+ if (persona) soul.identity['PERSONA.md'] = persona;
52
+
53
+ const map = readFile(path.join(atrisDir, 'MAP.md'));
54
+ if (map) soul.identity['MAP.md'] = map;
55
+
56
+ // Team members
57
+ const teamDir = path.join(atrisDir, 'team');
58
+ if (fs.existsSync(teamDir)) {
59
+ const members = fs.readdirSync(teamDir).filter(f => {
60
+ const memberPath = path.join(teamDir, f, 'MEMBER.md');
61
+ return fs.existsSync(memberPath);
62
+ });
63
+ soul.identity.team = members;
64
+ }
65
+
66
+ // Knowledge — features
67
+ const featuresDir = path.join(atrisDir, 'features');
68
+ if (fs.existsSync(featuresDir)) {
69
+ const features = fs.readdirSync(featuresDir).filter(f => {
70
+ return !f.startsWith('_') && fs.existsSync(path.join(featuresDir, f, 'idea.md'));
71
+ });
72
+ soul.knowledge.features = features;
73
+ soul.knowledge.feature_count = features.length;
74
+ }
75
+
76
+ // Knowledge — research
77
+ const researchDir = path.join(atrisDir, 'research');
78
+ if (fs.existsSync(researchDir)) {
79
+ soul.knowledge.research_files = countFiles(researchDir);
80
+ }
81
+
82
+ // Knowledge — refs
83
+ const refsDir = path.join(atrisDir, 'refs');
84
+ if (fs.existsSync(refsDir)) {
85
+ soul.knowledge.refs = fs.readdirSync(refsDir).filter(f => f.endsWith('.md'));
86
+ }
87
+
88
+ // Learned — policies
89
+ const policiesDir = path.join(atrisDir, 'policies');
90
+ if (fs.existsSync(policiesDir)) {
91
+ soul.learned.policies = fs.readdirSync(policiesDir).filter(f => f.endsWith('.md'));
92
+ }
93
+
94
+ // Learned — logs (journal depth)
95
+ const logsDir = path.join(atrisDir, 'logs');
96
+ if (fs.existsSync(logsDir)) {
97
+ soul.learned.journal_entries = countFiles(logsDir);
98
+ }
99
+
100
+ // Learned — lessons
101
+ const lessons = readFile(path.join(atrisDir, 'lessons.md'));
102
+ if (lessons) {
103
+ const lessonCount = (lessons.match(/^-/gm) || []).length;
104
+ soul.learned.lessons = lessonCount;
105
+ }
106
+
107
+ // Stats
108
+ const todo = readFile(path.join(atrisDir, 'TODO.md'));
109
+ if (todo) {
110
+ const tasks = (todo.match(/^- /gm) || []).length;
111
+ soul.stats.open_tasks = tasks;
112
+ }
113
+
114
+ soul.stats.identity_files = Object.keys(soul.identity).length;
115
+ soul.stats.knowledge_items = (soul.knowledge.feature_count || 0) + (soul.knowledge.research_files || 0);
116
+ soul.stats.learned_items = (soul.learned.journal_entries || 0) + (soul.learned.lessons || 0);
117
+
118
+ return soul;
119
+ }
120
+
121
+ // ── Display ────────────────────────────────────────────
122
+
123
+ function displaySoul(soul) {
124
+ const W = 50;
125
+ const line = '─'.repeat(W);
126
+
127
+ console.log(`\n ┌${line}┐`);
128
+ console.log(` │ ${'◉ SOUL — ' + soul.project}${' '.repeat(Math.max(0, W - 10 - soul.project.length))}│`);
129
+ console.log(` ├${line}┤`);
130
+
131
+ // Identity
132
+ console.log(` │ ${'IDENTITY'.padEnd(W - 1)}│`);
133
+ if (soul.identity['PERSONA.md']) {
134
+ const preview = soul.identity['PERSONA.md'].split('\n').find(l => l.trim() && !l.startsWith('#')) || '';
135
+ console.log(` │ persona: ${preview.slice(0, W - 15).padEnd(W - 14)}│`);
136
+ }
137
+ if (soul.identity.team) {
138
+ console.log(` │ team: ${soul.identity.team.length} members${' '.repeat(Math.max(0, W - 20 - String(soul.identity.team.length).length))}│`);
139
+ }
140
+
141
+ // Knowledge
142
+ console.log(` │ ${'KNOWLEDGE'.padEnd(W - 1)}│`);
143
+ if (soul.knowledge.feature_count) {
144
+ console.log(` │ features: ${soul.knowledge.feature_count}${' '.repeat(Math.max(0, W - 16 - String(soul.knowledge.feature_count).length))}│`);
145
+ }
146
+ if (soul.knowledge.research_files) {
147
+ console.log(` │ research: ${soul.knowledge.research_files} files${' '.repeat(Math.max(0, W - 22 - String(soul.knowledge.research_files).length))}│`);
148
+ }
149
+
150
+ // Learned
151
+ console.log(` │ ${'LEARNED'.padEnd(W - 1)}│`);
152
+ if (soul.learned.journal_entries) {
153
+ console.log(` │ journal: ${soul.learned.journal_entries} entries${' '.repeat(Math.max(0, W - 23 - String(soul.learned.journal_entries).length))}│`);
154
+ }
155
+ if (soul.learned.lessons) {
156
+ console.log(` │ lessons: ${soul.learned.lessons}${' '.repeat(Math.max(0, W - 15 - String(soul.learned.lessons).length))}│`);
157
+ }
158
+ if (soul.learned.policies) {
159
+ console.log(` │ policies: ${soul.learned.policies.length}${' '.repeat(Math.max(0, W - 16 - String(soul.learned.policies.length).length))}│`);
160
+ }
161
+
162
+ console.log(` └${line}┘\n`);
163
+ }
164
+
165
+ // ── Fork ───────────────────────────────────────────────
166
+
167
+ function forkSoul(sourceDir, targetDir) {
168
+ const toCopy = [
169
+ 'PERSONA.md',
170
+ 'policies',
171
+ 'refs',
172
+ ];
173
+
174
+ let copied = 0;
175
+ for (const item of toCopy) {
176
+ const src = path.join(sourceDir, item);
177
+ const dst = path.join(targetDir, item);
178
+ if (!fs.existsSync(src)) continue;
179
+
180
+ const stat = fs.statSync(src);
181
+ if (stat.isDirectory()) {
182
+ fs.mkdirSync(dst, { recursive: true });
183
+ for (const f of fs.readdirSync(src)) {
184
+ fs.copyFileSync(path.join(src, f), path.join(dst, f));
185
+ copied++;
186
+ }
187
+ } else {
188
+ fs.copyFileSync(src, dst);
189
+ copied++;
190
+ }
191
+ }
192
+
193
+ // Write genealogy
194
+ const genealogy = {
195
+ forked_from: path.basename(path.dirname(sourceDir)),
196
+ forked_at: new Date().toISOString(),
197
+ files_copied: copied,
198
+ };
199
+ fs.writeFileSync(path.join(targetDir, 'genealogy.json'), JSON.stringify(genealogy, null, 2));
200
+
201
+ return { copied, genealogy };
202
+ }
203
+
204
+ // ── Distill ────────────────────────────────────────────
205
+
206
+ function distillSoul(atrisDir) {
207
+ const personaPath = path.join(atrisDir, 'PERSONA.md');
208
+ const lessonsPath = path.join(atrisDir, 'lessons.md');
209
+ const policiesDir = path.join(atrisDir, 'policies');
210
+
211
+ // Gather learnings
212
+ const lessons = readFile(lessonsPath);
213
+ const persona = readFile(personaPath);
214
+
215
+ let policyRules = [];
216
+ if (fs.existsSync(policiesDir)) {
217
+ for (const f of fs.readdirSync(policiesDir).filter(f => f.endsWith('.md'))) {
218
+ const content = readFile(path.join(policiesDir, f));
219
+ if (content) {
220
+ // Extract bullet points as rules
221
+ const bullets = content.match(/^[-*]\s+.+$/gm) || [];
222
+ policyRules.push(...bullets.map(b => b.replace(/^[-*]\s+/, '').trim()));
223
+ }
224
+ }
225
+ }
226
+
227
+ // Extract lesson patterns
228
+ let lessonItems = [];
229
+ if (lessons) {
230
+ lessonItems = (lessons.match(/^[-*]\s+.+$/gm) || []).map(l => l.replace(/^[-*]\s+/, '').trim());
231
+ }
232
+
233
+ // Count journal entries for depth stat
234
+ const logsDir = path.join(atrisDir, 'logs');
235
+ let journalCount = 0;
236
+ if (fs.existsSync(logsDir)) {
237
+ journalCount = countFiles(logsDir);
238
+ }
239
+
240
+ // Build distilled section
241
+ const distilled = [];
242
+ if (lessonItems.length > 0) {
243
+ distilled.push('## Learned (distilled from lessons.md)');
244
+ distilled.push('');
245
+ // Deduplicate and take top 10 by recency (last in list = most recent)
246
+ const unique = [...new Set(lessonItems)].slice(-10);
247
+ for (const item of unique) {
248
+ distilled.push(`- ${item}`);
249
+ }
250
+ distilled.push('');
251
+ }
252
+
253
+ if (policyRules.length > 0) {
254
+ distilled.push('## Policies (distilled from policies/)');
255
+ distilled.push('');
256
+ const unique = [...new Set(policyRules)].slice(0, 10);
257
+ for (const rule of unique) {
258
+ distilled.push(`- ${rule}`);
259
+ }
260
+ distilled.push('');
261
+ }
262
+
263
+ if (distilled.length === 0) {
264
+ return { status: 'nothing', reason: 'No lessons or policies to distill' };
265
+ }
266
+
267
+ // Archive current persona
268
+ if (persona) {
269
+ const archiveDir = path.join(atrisDir, 'persona-history');
270
+ if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
271
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
272
+ fs.writeFileSync(path.join(archiveDir, `${ts}.md`), persona);
273
+ }
274
+
275
+ // Append distilled section to persona
276
+ const separator = '\n\n---\n\n';
277
+ const distilledBlock = `<!-- Distilled ${new Date().toISOString().slice(0, 10)} | ${journalCount} journal entries, ${lessonItems.length} lessons, ${policyRules.length} policy rules -->\n\n` + distilled.join('\n');
278
+
279
+ if (persona) {
280
+ // Check if already has a distilled section, replace it
281
+ if (persona.includes('<!-- Distilled')) {
282
+ const updated = persona.replace(/<!-- Distilled[\s\S]*$/, distilledBlock);
283
+ fs.writeFileSync(personaPath, updated);
284
+ } else {
285
+ fs.appendFileSync(personaPath, separator + distilledBlock);
286
+ }
287
+ } else {
288
+ fs.writeFileSync(personaPath, `# Persona\n\n${distilledBlock}`);
289
+ }
290
+
291
+ return {
292
+ status: 'distilled',
293
+ lessons: lessonItems.length,
294
+ policies: policyRules.length,
295
+ archived: !!persona,
296
+ };
297
+ }
298
+
299
+ // ── Main ───────────────────────────────────────────────
300
+
301
+ async function soul(args = []) {
302
+ const subcommand = (args[0] || 'status').toLowerCase();
303
+ const atrisDir = findAtrisDir();
304
+
305
+ if (!atrisDir) {
306
+ console.error('✗ No atris/ folder found. Run "atris init" first.');
307
+ process.exit(1);
308
+ }
309
+
310
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
311
+ console.log('');
312
+ console.log(' atris soul — see what your project has learned');
313
+ console.log('');
314
+ console.log(' soul show identity, knowledge, learnings');
315
+ console.log(' soul snapshot export full soul to JSON (auto-gitignored)');
316
+ console.log(' soul distill compress lessons + policies into PERSONA.md');
317
+ console.log(' soul fork <path> copy persona + policies to another project');
318
+ console.log('');
319
+ return;
320
+ }
321
+
322
+ switch (subcommand) {
323
+ case 'status':
324
+ case 'st': {
325
+ const s = snapshotSoul(atrisDir);
326
+ displaySoul(s);
327
+ break;
328
+ }
329
+ case 'snapshot':
330
+ case 'export': {
331
+ const s = snapshotSoul(atrisDir);
332
+ const outPath = path.join(atrisDir, 'soul-snapshot.json');
333
+ fs.writeFileSync(outPath, JSON.stringify(s, null, 2));
334
+ // Auto-add to .gitignore so it never gets committed
335
+ const gitignorePath = path.join(path.dirname(atrisDir), '.gitignore');
336
+ try {
337
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
338
+ if (!existing.includes('soul-snapshot.json')) {
339
+ fs.appendFileSync(gitignorePath, '\n# Atris soul — private, never commit\natris/soul-snapshot.json\n');
340
+ }
341
+ } catch {}
342
+ console.log(`✓ Soul snapshot saved to ${outPath}`);
343
+ console.log(` (auto-added to .gitignore — this stays private)`);
344
+ break;
345
+ }
346
+ case 'fork': {
347
+ const target = args[1];
348
+ if (!target) {
349
+ console.error('Usage: atris soul fork <target-project-path>');
350
+ return;
351
+ }
352
+ const targetAtris = path.join(target, 'atris');
353
+ if (!fs.existsSync(targetAtris)) {
354
+ console.error(`✗ Target ${targetAtris} not found. Run "atris init" in that project first.`);
355
+ return;
356
+ }
357
+ const result = forkSoul(atrisDir, targetAtris);
358
+ console.log(`✓ Soul forked: ${result.copied} files copied`);
359
+ console.log(` From: ${path.basename(path.dirname(atrisDir))}`);
360
+ console.log(` To: ${path.basename(target)}`);
361
+ break;
362
+ }
363
+ case 'distill':
364
+ case 'compress': {
365
+ const result = distillSoul(atrisDir);
366
+ if (result.status === 'nothing') {
367
+ console.log(`✗ ${result.reason}`);
368
+ } else {
369
+ console.log(`✓ Soul distilled`);
370
+ console.log(` ${result.lessons} lessons + ${result.policies} policy rules → PERSONA.md`);
371
+ if (result.archived) console.log(` Previous persona archived to persona-history/`);
372
+ }
373
+ break;
374
+ }
375
+ default:
376
+ const s = snapshotSoul(atrisDir);
377
+ displaySoul(s);
378
+ }
379
+ }
380
+
381
+ module.exports = { soul, snapshotSoul };
@@ -12,7 +12,71 @@ const pad = (str, w = W) => {
12
12
  return len >= w ? str : str + ' '.repeat(w - len);
13
13
  };
14
14
 
15
- function statusAtris(isQuick = false, jsonMode = false) {
15
+ function wrapText(text, width = 74) {
16
+ const normalized = String(text || '').replace(/\s+/g, ' ').trim();
17
+ if (!normalized) return [''];
18
+
19
+ const words = normalized.split(' ');
20
+ const lines = [];
21
+ let current = '';
22
+
23
+ for (const word of words) {
24
+ if (!current) {
25
+ current = word;
26
+ continue;
27
+ }
28
+ if ((current + ' ' + word).length <= width) {
29
+ current += ' ' + word;
30
+ } else {
31
+ lines.push(current);
32
+ current = word;
33
+ }
34
+ }
35
+
36
+ if (current) lines.push(current);
37
+ return lines;
38
+ }
39
+
40
+ function compactWrappedText(text, width = 74, maxLines = 2) {
41
+ const lines = wrapText(text, width);
42
+ if (lines.length <= maxLines) return lines;
43
+
44
+ const kept = lines.slice(0, maxLines);
45
+ const head = kept.slice(0, -1);
46
+ let tail = kept[kept.length - 1].replace(/[ .,;:!?-]+$/, '');
47
+ if (tail.length >= width) {
48
+ tail = tail.slice(0, width - 1).replace(/[ .,;:!?-]+$/, '');
49
+ }
50
+ return [...head, `${tail}…`];
51
+ }
52
+
53
+ function printHumanSection(title, body) {
54
+ console.log(` ${title}`);
55
+ const sourceLines = Array.isArray(body) ? body : String(body || '').split('\n');
56
+ for (const sourceLine of sourceLines) {
57
+ if (!sourceLine) {
58
+ console.log(' ');
59
+ continue;
60
+ }
61
+ for (const line of wrapText(sourceLine)) {
62
+ console.log(` ${line}`);
63
+ }
64
+ }
65
+ console.log('');
66
+ }
67
+
68
+ function readEndgameMeta(todoFile) {
69
+ if (!fs.existsSync(todoFile)) return { slug: null, horizon: null };
70
+ const todoContent = fs.readFileSync(todoFile, 'utf8');
71
+ const endgameMatch = todoContent.match(/##\s+Endgame\s*\n([\s\S]*?)(?=\n##|$)/);
72
+ if (!endgameMatch) return { slug: null, horizon: null };
73
+
74
+ const slug = endgameMatch[1].match(/\*\*Slug:\*\*\s*(.+)/)?.[1]?.trim() || null;
75
+ const horizon = endgameMatch[1].match(/\*\*Horizon:\*\*\s*(.+)/)?.[1]?.trim() || null;
76
+ return { slug, horizon };
77
+ }
78
+
79
+ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
16
80
  const targetDir = path.join(process.cwd(), 'atris');
17
81
 
18
82
  if (!fs.existsSync(targetDir)) {
@@ -114,6 +178,60 @@ function statusAtris(isQuick = false, jsonMode = false) {
114
178
  return;
115
179
  }
116
180
 
181
+ if (!verbose) {
182
+ const endgame = readEndgameMeta(todoFile);
183
+ const where = [];
184
+ if (endgame.slug) {
185
+ where.push(`The active horizon is ${endgame.slug}.`);
186
+ if (endgame.horizon) {
187
+ where.push(...compactWrappedText(endgame.horizon, 74, 2));
188
+ }
189
+ } else {
190
+ where.push('No active endgame is set.');
191
+ }
192
+ where.push(`There are ${todo.inProgress.length} tasks in progress, ${todo.backlog.length} queued, and ${todo.completed.length} completed items still sitting in TODO.`);
193
+
194
+ const queueParts = [];
195
+ if (todo.inProgress[0]) {
196
+ queueParts.push(...compactWrappedText(`In progress: ${todo.inProgress[0].title}.`, 74, 2));
197
+ } else {
198
+ queueParts.push('In progress: none.');
199
+ }
200
+ if (todo.backlog[0]) {
201
+ queueParts.push(...compactWrappedText(`Next backlog item: ${todo.backlog[0].title}.`, 74, 2));
202
+ } else {
203
+ queueParts.push('Next backlog item: none.');
204
+ }
205
+ queueParts.push(inboxItems.length > 0
206
+ ? `Inbox has ${inboxItems.length} item${inboxItems.length === 1 ? '' : 's'}.`
207
+ : 'Inbox is empty.');
208
+
209
+ const blockingParts = [];
210
+ if (todo.completed.length > 0) {
211
+ blockingParts.push(`Main drag: ${todo.completed.length} completed item${todo.completed.length === 1 ? '' : 's'} should be cleared from TODO.`);
212
+ } else {
213
+ blockingParts.push('No cleanup debt is visible in TODO.');
214
+ }
215
+ if (todo.inProgress.length === 0 && todo.backlog.length === 0 && inboxItems.length === 0) {
216
+ blockingParts.push('No active blocker is visible right now.');
217
+ } else if (teamActivity.length === 0) {
218
+ blockingParts.push('No team activity is logged yet today.');
219
+ } else {
220
+ blockingParts.push(`Team activity has ${teamActivity.length} recent signal${teamActivity.length === 1 ? '' : 's'}.`);
221
+ }
222
+ blockingParts.push(
223
+ todo.completed.length > 0
224
+ ? 'Decision: let it run unless you want cleanup debt handled first.'
225
+ : 'Decision: let it run.'
226
+ );
227
+
228
+ console.log('');
229
+ printHumanSection('Where we are:', where);
230
+ printHumanSection('What is queued:', queueParts);
231
+ printHumanSection('What is blocking:', blockingParts);
232
+ return;
233
+ }
234
+
117
235
  // ─── FULL VISUAL STATUS ────────────────────────────────────
118
236
  const o = (s) => console.log(s);
119
237
 
package/commands/sync.js CHANGED
@@ -1,8 +1,142 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const { ensureWikiScaffold } = require('../lib/wiki');
5
+
6
+ const BUSINESS_TEMPLATE_DIR = path.join(__dirname, '..', 'templates', 'business-canonical');
7
+
8
+ /**
9
+ * Walk a directory and return relative file paths.
10
+ */
11
+ function _walkTemplateDir(dir, base = dir) {
12
+ const out = [];
13
+ let entries;
14
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
15
+ catch { return out; }
16
+ for (const e of entries) {
17
+ const full = path.join(dir, e.name);
18
+ if (e.isDirectory()) {
19
+ out.push(..._walkTemplateDir(full, base));
20
+ } else if (e.isFile()) {
21
+ out.push(path.relative(base, full));
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+
27
+ /**
28
+ * Substitute {{name}}, {{slug}}, {{owner_email}} in template content.
29
+ */
30
+ function _substituteParams(content, params) {
31
+ return content
32
+ .replace(/\{\{name\}\}/g, params.name || params.slug || 'this business')
33
+ .replace(/\{\{slug\}\}/g, params.slug || 'business')
34
+ .replace(/\{\{owner_email\}\}/g, params.owner_email || '');
35
+ }
36
+
37
+ /**
38
+ * Sync canonical business template files into a business workspace.
39
+ * Used when .atris/business.json is present (business mode).
40
+ *
41
+ * Default: NEVER overwrites existing files (preserves customizations).
42
+ * --force: overwrites existing canonical files (bumps to latest).
43
+ */
44
+ function syncBusinessCanonical(targetRoot, bizMeta) {
45
+ const params = {
46
+ slug: bizMeta.slug || 'business',
47
+ name: bizMeta.name || bizMeta.slug || 'this business',
48
+ owner_email: bizMeta.owner_email || '',
49
+ };
50
+ const force = process.argv.includes('--force');
51
+ const dryRun = process.argv.includes('--dry-run');
52
+ const targetAtrisDir = path.join(targetRoot, 'atris');
53
+
54
+ if (!fs.existsSync(BUSINESS_TEMPLATE_DIR)) {
55
+ console.error(`✗ Canonical template directory not found: ${BUSINESS_TEMPLATE_DIR}`);
56
+ console.error(' Your atris-cli installation may be incomplete.');
57
+ process.exit(1);
58
+ }
59
+
60
+ console.log('');
61
+ console.log(`Updating ${params.name} (${params.slug}) from canonical templates...`);
62
+ console.log(` Target: ${targetAtrisDir}/`);
63
+ console.log(` Source: ${BUSINESS_TEMPLATE_DIR}`);
64
+ console.log('');
65
+
66
+ const templateFiles = _walkTemplateDir(BUSINESS_TEMPLATE_DIR).sort();
67
+ let added = 0, updated = 0, skipped = 0, preserved = 0;
68
+ const addedList = [], updatedList = [], preservedList = [];
69
+
70
+ for (const relPath of templateFiles) {
71
+ const templatePath = path.join(BUSINESS_TEMPLATE_DIR, relPath);
72
+ const targetPath = path.join(targetAtrisDir, relPath);
73
+ let templateContent;
74
+ try { templateContent = fs.readFileSync(templatePath, 'utf-8'); } catch { continue; }
75
+ const finalContent = _substituteParams(templateContent, params);
76
+
77
+ if (!fs.existsSync(targetPath)) {
78
+ addedList.push(relPath); added++;
79
+ if (!dryRun) {
80
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
81
+ fs.writeFileSync(targetPath, finalContent);
82
+ }
83
+ } else {
84
+ const existing = fs.readFileSync(targetPath, 'utf-8');
85
+ if (existing === finalContent) {
86
+ skipped++;
87
+ } else if (force) {
88
+ updatedList.push(relPath); updated++;
89
+ if (!dryRun) fs.writeFileSync(targetPath, finalContent);
90
+ } else {
91
+ preservedList.push(relPath); preserved++;
92
+ }
93
+ }
94
+ }
95
+
96
+ console.log(` Added: ${added}`);
97
+ console.log(` Updated: ${updated} ${force ? '' : '(--force to enable)'}`);
98
+ console.log(` Preserved: ${preserved} (existing customizations kept)`);
99
+ console.log(` Skipped: ${skipped} (already match template)`);
100
+ console.log('');
101
+
102
+ if (addedList.length > 0) {
103
+ console.log(' New files:');
104
+ addedList.slice(0, 15).forEach(p => console.log(` + atris/${p}`));
105
+ if (addedList.length > 15) console.log(` ... +${addedList.length - 15} more`);
106
+ console.log('');
107
+ }
108
+ if (updatedList.length > 0) {
109
+ console.log(` ${force ? 'Updated' : 'Differ from template (preserved)'}:`);
110
+ updatedList.slice(0, 15).forEach(p => console.log(` ↑ atris/${p}`));
111
+ if (updatedList.length > 15) console.log(` ... +${updatedList.length - 15} more`);
112
+ console.log('');
113
+ }
114
+
115
+ if (dryRun) {
116
+ console.log(' (--dry-run, no changes made)');
117
+ } else if (added === 0 && updated === 0) {
118
+ ensureWikiScaffold(targetRoot);
119
+ console.log(' ✓ Already up to date');
120
+ } else {
121
+ ensureWikiScaffold(targetRoot);
122
+ console.log(` ✓ Local workspace updated. Run \`atris align ${params.slug} --fix\` to push to EC2.`);
123
+ }
124
+ }
4
125
 
5
126
  function syncAtris() {
127
+ // Business mode detection: if .atris/business.json exists, use canonical templates
128
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
129
+ if (fs.existsSync(bizFile)) {
130
+ try {
131
+ const bizMeta = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
132
+ return syncBusinessCanonical(process.cwd(), bizMeta);
133
+ } catch (e) {
134
+ console.error(`✗ Failed to read .atris/business.json: ${e.message}`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ // Legacy/dev mode: sync from atris-cli's own atris/ folder
6
140
  const targetDir = path.join(process.cwd(), 'atris');
7
141
  const teamDir = path.join(targetDir, 'team');
8
142
  const legacyAgentTeamDir = path.join(targetDir, 'agent_team');
@@ -297,8 +431,10 @@ After displaying the boot output, respond to the user naturally.
297
431
  }
298
432
 
299
433
  if (updated === 0) {
434
+ ensureWikiScaffold(process.cwd());
300
435
  console.log('✓ Already up to date');
301
436
  } else {
437
+ ensureWikiScaffold(process.cwd());
302
438
  console.log(`\n✓ Updated ${updated} file(s), ${skipped} unchanged`);
303
439
  console.log('\nRun your AI agent again to use the latest specs and agent templates.');
304
440
  }
@@ -378,7 +514,17 @@ function syncSkills({ silent = false } = {}) {
378
514
  }
379
515
  }
380
516
 
381
- // --- 2. Project-level (only if inside an atris project) ---
517
+ // --- 2. Project-level (only if inside an atris project AND not a business workspace) ---
518
+ // BUSINESS GATE: don't sync framework skills into business workspaces.
519
+ // Per the canonical-layout decision, framework skills (autopilot, wiki, loop, etc.) live
520
+ // at the system level on EC2, NOT inside per-business workspaces. Customer workspaces
521
+ // contain ONLY business-specific custom skills in atris/skills/.
522
+ const businessJson = path.join(process.cwd(), '.atris', 'business.json');
523
+ if (fs.existsSync(businessJson)) {
524
+ // We're inside a business workspace — skip project-level skill sync.
525
+ return updated;
526
+ }
527
+
382
528
  const targetDir = path.join(process.cwd(), 'atris');
383
529
  if (fs.existsSync(targetDir)) {
384
530
  const userSkillsDir = path.join(targetDir, 'skills');