atris 2.6.3 → 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.
- package/README.md +124 -34
- package/atris/CLAUDE.md +5 -1
- package/atris/atris.md +4 -0
- package/atris/features/README.md +24 -0
- package/atris/skills/autopilot/SKILL.md +74 -75
- package/atris/skills/endgame/SKILL.md +179 -0
- package/atris/skills/flow/SKILL.md +121 -0
- package/atris/skills/improve/SKILL.md +84 -0
- package/atris/skills/loop/SKILL.md +72 -0
- package/atris/skills/wiki/SKILL.md +61 -0
- package/atris/team/executor/MEMBER.md +10 -4
- package/atris/team/navigator/MEMBER.md +2 -0
- package/atris/team/validator/MEMBER.md +8 -5
- package/atris.md +33 -0
- package/bin/atris.js +210 -41
- package/commands/activate.js +28 -2
- package/commands/align.js +720 -0
- package/commands/auth.js +75 -2
- package/commands/autopilot.js +1213 -270
- package/commands/browse.js +100 -0
- package/commands/business.js +785 -12
- package/commands/clean.js +107 -2
- package/commands/computer.js +429 -0
- package/commands/context-sync.js +78 -8
- package/commands/experiments.js +351 -0
- package/commands/feedback.js +150 -0
- package/commands/fleet.js +395 -0
- package/commands/fork.js +127 -0
- package/commands/init.js +50 -1
- package/commands/learn.js +407 -0
- package/commands/lifecycle.js +94 -0
- package/commands/loop.js +114 -0
- package/commands/publish.js +129 -0
- package/commands/pull.js +369 -38
- package/commands/push.js +283 -246
- package/commands/review.js +149 -0
- package/commands/run.js +76 -43
- package/commands/serve.js +360 -0
- package/commands/setup.js +1 -1
- package/commands/soul.js +381 -0
- package/commands/status.js +119 -1
- package/commands/sync.js +147 -1
- package/commands/terminal.js +201 -0
- package/commands/wiki.js +376 -0
- package/commands/workflow.js +191 -74
- package/commands/workspace-clean.js +3 -3
- package/lib/endstate.js +259 -0
- package/lib/learnings.js +235 -0
- package/lib/manifest.js +1 -0
- package/lib/todo.js +9 -5
- package/lib/wiki.js +578 -0
- package/package.json +2 -2
- package/utils/api.js +40 -35
- package/utils/auth.js +1 -0
package/commands/soul.js
ADDED
|
@@ -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 };
|
package/commands/status.js
CHANGED
|
@@ -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
|
|
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');
|