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,407 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+ const {
5
+ TYPES,
6
+ SOURCES,
7
+ loadLearnings,
8
+ addLearning,
9
+ searchLearnings,
10
+ findPruneTargets,
11
+ removeLearning,
12
+ getStats,
13
+ exportMarkdown,
14
+ } = require('../lib/learnings');
15
+
16
+ function showRecent(limit = 20) {
17
+ const learnings = loadLearnings()
18
+ .filter(e => e._effectiveConfidence > 0 && e.insight !== '[REMOVED]')
19
+ .slice(0, limit);
20
+
21
+ if (learnings.length === 0) {
22
+ console.log('');
23
+ console.log(' No learnings yet.');
24
+ console.log(' As you work, use "atris learn add" to capture patterns and pitfalls.');
25
+ console.log(' Or let your agents capture them during review cycles.');
26
+ console.log('');
27
+ return;
28
+ }
29
+
30
+ // Group by type
31
+ const byType = {};
32
+ for (const e of learnings) {
33
+ if (!byType[e.type]) byType[e.type] = [];
34
+ byType[e.type].push(e);
35
+ }
36
+
37
+ console.log('');
38
+ console.log('┌─────────────────────────────────────────────────────────────┐');
39
+ console.log(`│ Learnings — ${learnings.length} active${' '.repeat(Math.max(0, 44 - String(learnings.length).length))}│`);
40
+ console.log('└─────────────────────────────────────────────────────────────┘');
41
+ console.log('');
42
+
43
+ for (const [type, entries] of Object.entries(byType)) {
44
+ console.log(` ${type.toUpperCase()}S`);
45
+ for (const e of entries) {
46
+ const conf = e._effectiveConfidence;
47
+ const bar = conf >= 7 ? '●' : conf >= 4 ? '◐' : '○';
48
+ const date = (e.ts || '').split('T')[0];
49
+ console.log(` ${bar} [${conf}/10] ${e.key} — ${e.insight}`);
50
+ if (e.files && e.files.length > 0) {
51
+ console.log(` files: ${e.files.join(', ')}`);
52
+ }
53
+ }
54
+ console.log('');
55
+ }
56
+ }
57
+
58
+ function showSearch(query) {
59
+ if (!query) {
60
+ console.log(' Usage: atris learn search <query>');
61
+ return;
62
+ }
63
+
64
+ const results = searchLearnings(query);
65
+ if (results.length === 0) {
66
+ console.log(` No learnings matching "${query}"`);
67
+ return;
68
+ }
69
+
70
+ console.log('');
71
+ console.log(` Search: "${query}" — ${results.length} result(s)`);
72
+ console.log('');
73
+ for (const e of results) {
74
+ const conf = e._effectiveConfidence;
75
+ const bar = conf >= 7 ? '●' : conf >= 4 ? '◐' : '○';
76
+ console.log(` ${bar} [${conf}/10] ${e.type}/${e.key} — ${e.insight}`);
77
+ }
78
+ console.log('');
79
+ }
80
+
81
+ function showStats() {
82
+ const stats = getStats();
83
+
84
+ console.log('');
85
+ console.log('┌─────────────────────────────────────────────────────────────┐');
86
+ console.log('│ Learning Stats │');
87
+ console.log('└─────────────────────────────────────────────────────────────┘');
88
+ console.log('');
89
+ console.log(` Total: ${stats.total}`);
90
+ console.log(` Avg confidence: ${stats.avgConfidence}/10`);
91
+ console.log(` High (7+): ${stats.high}`);
92
+ console.log(` Medium (4-6): ${stats.medium}`);
93
+ console.log(` Low (1-3): ${stats.low}`);
94
+ console.log('');
95
+
96
+ if (Object.keys(stats.byType).length > 0) {
97
+ console.log(' By type:');
98
+ for (const [type, count] of Object.entries(stats.byType)) {
99
+ console.log(` ${type}: ${count}`);
100
+ }
101
+ console.log('');
102
+ }
103
+
104
+ if (Object.keys(stats.bySource).length > 0) {
105
+ console.log(' By source:');
106
+ for (const [source, count] of Object.entries(stats.bySource)) {
107
+ console.log(` ${source}: ${count}`);
108
+ }
109
+ console.log('');
110
+ }
111
+ }
112
+
113
+ function showExport() {
114
+ const md = exportMarkdown();
115
+ console.log('');
116
+ console.log(md);
117
+ console.log(' — Copy the above into CLAUDE.md or save to a file.');
118
+ console.log('');
119
+ }
120
+
121
+ function showPrune() {
122
+ const { stale, contradictions } = findPruneTargets();
123
+
124
+ if (stale.length === 0 && contradictions.length === 0) {
125
+ console.log('');
126
+ console.log(' ✓ All learnings are healthy. No stale entries or contradictions.');
127
+ console.log('');
128
+ return;
129
+ }
130
+
131
+ console.log('');
132
+ if (stale.length > 0) {
133
+ console.log(` STALE (${stale.length} — referenced files deleted):`);
134
+ for (const { entry, missingFiles } of stale) {
135
+ console.log(` ⚠ ${entry.key} — missing: ${missingFiles.join(', ')}`);
136
+ }
137
+ console.log('');
138
+ }
139
+
140
+ if (contradictions.length > 0) {
141
+ console.log(` CONFLICTS (${contradictions.length} — same key, different insight):`);
142
+ for (const { a, b } of contradictions) {
143
+ console.log(` ⚠ ${a.key}: "${a.insight}" vs "${b.insight}"`);
144
+ }
145
+ console.log('');
146
+ }
147
+
148
+ console.log(' Run "atris learn add" to update entries, or manually edit atris/learnings.jsonl');
149
+ console.log('');
150
+ }
151
+
152
+ function interactiveAdd() {
153
+ const rl = readline.createInterface({
154
+ input: process.stdin,
155
+ output: process.stdout,
156
+ });
157
+
158
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
159
+
160
+ console.log('');
161
+ console.log('┌─────────────────────────────────────────────────────────────┐');
162
+ console.log('│ Add Learning │');
163
+ console.log('└─────────────────────────────────────────────────────────────┘');
164
+ console.log('');
165
+ console.log(` Types: ${TYPES.join(', ')}`);
166
+ console.log('');
167
+
168
+ (async () => {
169
+ try {
170
+ const type = (await ask(' Type: ')).trim().toLowerCase();
171
+ if (!TYPES.includes(type)) {
172
+ console.log(` ✗ Invalid type. Must be one of: ${TYPES.join(', ')}`);
173
+ rl.close();
174
+ return;
175
+ }
176
+
177
+ const key = (await ask(' Key (2-5 words, kebab-case): ')).trim();
178
+ if (!key) {
179
+ console.log(' ✗ Key required.');
180
+ rl.close();
181
+ return;
182
+ }
183
+
184
+ const insight = (await ask(' Insight (one sentence): ')).trim();
185
+ if (!insight) {
186
+ console.log(' ✗ Insight required.');
187
+ rl.close();
188
+ return;
189
+ }
190
+
191
+ const confStr = (await ask(' Confidence (1-10): ')).trim();
192
+ const confidence = parseInt(confStr, 10);
193
+ if (isNaN(confidence) || confidence < 1 || confidence > 10) {
194
+ console.log(' ✗ Confidence must be 1-10.');
195
+ rl.close();
196
+ return;
197
+ }
198
+
199
+ const source = (await ask(` Source (${SOURCES.join('/')}): `)).trim().toLowerCase();
200
+ if (!SOURCES.includes(source)) {
201
+ console.log(` ✗ Invalid source. Must be one of: ${SOURCES.join(', ')}`);
202
+ rl.close();
203
+ return;
204
+ }
205
+
206
+ const filesStr = (await ask(' Related files (comma-separated, or empty): ')).trim();
207
+ const files = filesStr ? filesStr.split(',').map(f => f.trim()).filter(Boolean) : [];
208
+
209
+ // Quality gate
210
+ const worth = (await ask('\n Would this save time in a future session? (y/n): ')).trim().toLowerCase();
211
+ if (worth !== 'y' && worth !== 'yes') {
212
+ console.log(' Skipped — only save learnings that compound.');
213
+ rl.close();
214
+ return;
215
+ }
216
+
217
+ const entry = addLearning({ type, key, insight, confidence, source, files });
218
+ console.log('');
219
+ console.log(` ✓ Saved: [${entry.confidence}/10] ${entry.type}/${entry.key}`);
220
+ console.log(` "${entry.insight}"`);
221
+ console.log('');
222
+ rl.close();
223
+ } catch (err) {
224
+ console.log(` ✗ Error: ${err.message}`);
225
+ rl.close();
226
+ }
227
+ })();
228
+ }
229
+
230
+ /**
231
+ * Non-interactive log: `atris learn log '{"type":"pattern","key":"...","insight":"...","confidence":8,"source":"observed"}'`
232
+ * For agents and scripts — no prompts, no quality gate.
233
+ */
234
+ function logDirect(jsonStr) {
235
+ if (!jsonStr) {
236
+ console.error(' ✗ Usage: atris learn log \'{"type":"...","key":"...","insight":"...","confidence":N,"source":"..."}\'');
237
+ process.exit(1);
238
+ }
239
+ try {
240
+ const data = JSON.parse(jsonStr);
241
+ const entry = addLearning({
242
+ type: data.type,
243
+ key: data.key,
244
+ insight: data.insight,
245
+ confidence: data.confidence || 5,
246
+ source: data.source || 'observed',
247
+ files: data.files || [],
248
+ });
249
+ console.log(` ✓ [${entry.confidence}/10] ${entry.type}/${entry.key}`);
250
+ } catch (err) {
251
+ console.error(` ✗ ${err.message}`);
252
+ process.exit(1);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Harvest learnings from journal Notes sections.
258
+ * Scans recent journals for lines that look like insights.
259
+ */
260
+ function harvestFromJournals() {
261
+ const atrisDir = path.join(process.cwd(), 'atris');
262
+ const logsDir = path.join(atrisDir, 'logs');
263
+
264
+ if (!fs.existsSync(logsDir)) {
265
+ console.log(' No journals found.');
266
+ return;
267
+ }
268
+
269
+ // Find all journal files, newest first
270
+ const allLogs = [];
271
+ const yearDirs = fs.readdirSync(logsDir).filter(d => /^\d{4}$/.test(d));
272
+ for (const year of yearDirs) {
273
+ const yearPath = path.join(logsDir, year);
274
+ if (fs.statSync(yearPath).isDirectory()) {
275
+ const files = fs.readdirSync(yearPath).filter(f => f.endsWith('.md'));
276
+ files.forEach(f => allLogs.push(path.join(yearPath, f)));
277
+ }
278
+ }
279
+ allLogs.sort().reverse();
280
+
281
+ // Scan last 7 journals for Notes section entries
282
+ const candidates = [];
283
+ for (const logPath of allLogs.slice(0, 7)) {
284
+ const content = fs.readFileSync(logPath, 'utf8');
285
+ const notesMatch = content.match(/## Notes\n([\s\S]*?)(?=\n## |$)/);
286
+ if (notesMatch && notesMatch[1].trim()) {
287
+ const lines = notesMatch[1].trim().split('\n').filter(l => l.startsWith('- '));
288
+ for (const line of lines) {
289
+ // Strip bullet and optional timestamp prefix
290
+ const insight = line.replace(/^- (\d{2}:\d{2} — )?/, '').trim();
291
+ if (insight.length > 10) {
292
+ candidates.push({ insight, source: path.basename(logPath) });
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ if (candidates.length === 0) {
299
+ console.log('');
300
+ console.log(' No harvestable notes found in recent journals.');
301
+ console.log(' Add notes during "atris review" or write to ## Notes in your journal.');
302
+ console.log('');
303
+ return;
304
+ }
305
+
306
+ // Check which are already in learnings
307
+ const existing = loadLearnings();
308
+ const existingInsights = new Set(existing.map(e => e.insight.toLowerCase()));
309
+ const fresh = candidates.filter(c => !existingInsights.has(c.insight.toLowerCase()));
310
+
311
+ if (fresh.length === 0) {
312
+ console.log('');
313
+ console.log(` Scanned ${candidates.length} journal notes — all already captured.`);
314
+ console.log('');
315
+ return;
316
+ }
317
+
318
+ console.log('');
319
+ console.log(` Found ${fresh.length} new note(s) to harvest:`);
320
+ console.log('');
321
+ for (let i = 0; i < fresh.length; i++) {
322
+ const c = fresh[i];
323
+ const isPitfall = /^(don't|never|avoid|watch out|careful)/i.test(c.insight);
324
+ const type = isPitfall ? 'pitfall' : 'pattern';
325
+ const key = c.insight.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).slice(0, 4).join('-');
326
+ console.log(` ${i + 1}. [${type}] ${c.insight}`);
327
+ console.log(` from: ${c.source}`);
328
+
329
+ try {
330
+ addLearning({ type, key, insight: c.insight, confidence: 6, source: 'review', files: [] });
331
+ console.log(` ✓ saved [6/10]`);
332
+ } catch (err) {
333
+ console.log(` ✗ ${err.message}`);
334
+ }
335
+ }
336
+ console.log('');
337
+ }
338
+
339
+ /**
340
+ * Get learning count for integration with atris activate.
341
+ */
342
+ function getLearningCount() {
343
+ const all = loadLearnings().filter(e => e._effectiveConfidence > 0 && e.insight !== '[REMOVED]');
344
+ return all.length;
345
+ }
346
+
347
+ /**
348
+ * Main entry point for `atris learn [subcommand] [args]`
349
+ */
350
+ function learnAtris(subcommand, ...args) {
351
+ const atrisDir = path.join(process.cwd(), 'atris');
352
+ if (!fs.existsSync(atrisDir)) {
353
+ console.error(' ✗ atris/ folder not found. Run "atris init" first.');
354
+ process.exit(1);
355
+ }
356
+
357
+ switch (subcommand) {
358
+ case undefined:
359
+ case '':
360
+ showRecent();
361
+ break;
362
+ case 'add':
363
+ interactiveAdd();
364
+ break;
365
+ case 'log':
366
+ logDirect(args[0]);
367
+ break;
368
+ case 'search':
369
+ showSearch(args.join(' '));
370
+ break;
371
+ case 'prune':
372
+ showPrune();
373
+ break;
374
+ case 'stats':
375
+ showStats();
376
+ break;
377
+ case 'export':
378
+ showExport();
379
+ break;
380
+ case 'count':
381
+ console.log(getLearningCount());
382
+ break;
383
+ case 'harvest':
384
+ harvestFromJournals();
385
+ break;
386
+ default:
387
+ console.log('');
388
+ console.log(' Usage: atris learn [command]');
389
+ console.log('');
390
+ console.log(' Commands:');
391
+ console.log(' (none) Show recent learnings');
392
+ console.log(' add Add a learning interactively');
393
+ console.log(' log <json> Add programmatically (for agents)');
394
+ console.log(' search <q> Search learnings by keyword');
395
+ console.log(' harvest Extract learnings from journal Notes');
396
+ console.log(' prune Check for stale/contradictory entries');
397
+ console.log(' stats Show learning statistics');
398
+ console.log(' export Export as markdown');
399
+ console.log(' count Print learning count (for integrations)');
400
+ console.log('');
401
+ break;
402
+ }
403
+ }
404
+
405
+ learnAtris.getLearningCount = getLearningCount;
406
+
407
+ module.exports = learnAtris;
@@ -0,0 +1,94 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadCredentials } = require('../utils/auth');
4
+ const { apiRequestJson } = require('../utils/api');
5
+
6
+ function resolveSlug() {
7
+ let slug = process.argv[3];
8
+ if (!slug || slug.startsWith('-')) {
9
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
10
+ if (fs.existsSync(bizFile)) {
11
+ try {
12
+ const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
13
+ slug = biz.slug || biz.name;
14
+ } catch {}
15
+ }
16
+ if (!slug || slug.startsWith('-')) slug = null;
17
+ }
18
+ return slug;
19
+ }
20
+
21
+ async function sleepAtris() {
22
+ const slug = resolveSlug();
23
+
24
+ if (!slug || slug === '--help') {
25
+ console.log('Usage: atris sleep [business]');
26
+ console.log('');
27
+ console.log(' Pause a workspace to save compute. Storage only.');
28
+ process.exit(0);
29
+ }
30
+
31
+ const creds = loadCredentials();
32
+ if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
33
+
34
+ const result = await apiRequestJson(`/workspace/${slug}/sleep`, {
35
+ method: 'POST',
36
+ token: creds.token,
37
+ });
38
+
39
+ if (!result.ok) {
40
+ console.error(`Failed to sleep workspace: ${result.error || result.status}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ console.log(`Workspace '${slug}' is now sleeping. Context saved. Wake it with: atris wake ${slug}`);
45
+ console.log('Compute paused. Storage only — pennies/day.');
46
+ }
47
+
48
+ async function wakeAtris() {
49
+ const slug = resolveSlug();
50
+
51
+ if (!slug || slug === '--help') {
52
+ console.log('Usage: atris wake [business]');
53
+ console.log('');
54
+ console.log(' Wake a sleeping workspace. Agents resume automatically.');
55
+ process.exit(0);
56
+ }
57
+
58
+ const creds = loadCredentials();
59
+ if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
60
+
61
+ const result = await apiRequestJson(`/workspace/${slug}/wake`, {
62
+ method: 'POST',
63
+ token: creds.token,
64
+ });
65
+
66
+ if (!result.ok) {
67
+ console.error(`Failed to wake workspace: ${result.error || result.status}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ console.log(`Waking '${slug}'...`);
72
+
73
+ const maxWait = 30000;
74
+ const interval = 2000;
75
+ const start = Date.now();
76
+
77
+ while (Date.now() - start < maxWait) {
78
+ await new Promise(r => setTimeout(r, interval));
79
+
80
+ const status = await apiRequestJson(`/workspace/${slug}/status`, {
81
+ method: 'GET',
82
+ token: creds.token,
83
+ });
84
+
85
+ if (status.ok && status.data && status.data.status === 'running') {
86
+ console.log(`Workspace '${slug}' is alive. Agents resuming.`);
87
+ return;
88
+ }
89
+ }
90
+
91
+ console.log('Still starting up. Check with: atris status');
92
+ }
93
+
94
+ module.exports = { sleepAtris, wakeAtris };
@@ -0,0 +1,114 @@
1
+ const path = require('path');
2
+ const {
3
+ WIKI_ROOT,
4
+ ensureWikiScaffold,
5
+ readWikiPages,
6
+ findStaleWikiPages,
7
+ findWikiOrphans,
8
+ findSuggestedSources,
9
+ writeWikiStatus,
10
+ appendWikiLog,
11
+ } = require('../lib/wiki');
12
+
13
+ function formatPageList(items, limit = 3) {
14
+ return items.slice(0, limit).map((item) => item.page || item);
15
+ }
16
+
17
+ function buildReport(projectRoot = process.cwd(), limit = 3) {
18
+ ensureWikiScaffold(projectRoot);
19
+ const pages = readWikiPages(projectRoot);
20
+ const stalePages = findStaleWikiPages(projectRoot);
21
+ const orphanPages = findWikiOrphans(projectRoot);
22
+ const nextSources = findSuggestedSources(projectRoot, limit);
23
+
24
+ let health = 'wiki is in good shape';
25
+ let nextMove = 'run `atris query "..."` or ingest a new source';
26
+
27
+ if (stalePages.length > 0) {
28
+ health = `${stalePages.length} stale page${stalePages.length === 1 ? '' : 's'} need recompiling`;
29
+ nextMove = `recompile ${stalePages[0].page} from ${stalePages[0].staleSource}`;
30
+ } else if (orphanPages.length > 0) {
31
+ health = `${orphanPages.length} orphan page${orphanPages.length === 1 ? '' : 's'} need linking or index coverage`;
32
+ nextMove = `link or index ${orphanPages[0]}`;
33
+ } else if (nextSources.length > 0) {
34
+ health = `wiki is stable, ${nextSources.length} high-value source${nextSources.length === 1 ? '' : 's'} ready for ingest`;
35
+ nextMove = `ingest ${nextSources[0]}`;
36
+ }
37
+
38
+ return {
39
+ wikiDir: path.join(projectRoot, WIKI_ROOT),
40
+ pageCount: pages.length,
41
+ stalePages,
42
+ orphanPages,
43
+ nextSources,
44
+ health,
45
+ nextMove,
46
+ details: [
47
+ `pages=${pages.length}`,
48
+ `stale=${stalePages.length}`,
49
+ `orphans=${orphanPages.length}`,
50
+ `suggested=${nextSources.length}`,
51
+ ],
52
+ };
53
+ }
54
+
55
+ function printReport(report) {
56
+ console.log('');
57
+ console.log('┌─────────────────────────────────────────────────────────────┐');
58
+ console.log('│ Wiki Loop │');
59
+ console.log('├─────────────────────────────────────────────────────────────┤');
60
+ const summary = `Pages: ${report.pageCount} | Stale: ${report.stalePages.length} | Orphans: ${report.orphanPages.length} | Next: ${report.nextSources.length}`;
61
+ console.log(`│ ${summary.padEnd(59)} │`);
62
+ console.log('└─────────────────────────────────────────────────────────────┘');
63
+ console.log('');
64
+ console.log(`Health: ${report.health}`);
65
+ console.log(`Next move: ${report.nextMove}`);
66
+ if (report.stalePages.length > 0) {
67
+ console.log('');
68
+ console.log('Stale pages:');
69
+ formatPageList(report.stalePages).forEach((page) => console.log(`- ${page}`));
70
+ }
71
+ if (report.orphanPages.length > 0) {
72
+ console.log('');
73
+ console.log('Orphans:');
74
+ formatPageList(report.orphanPages).forEach((page) => console.log(`- ${page}`));
75
+ }
76
+ if (report.nextSources.length > 0) {
77
+ console.log('');
78
+ console.log('Next ingest candidates:');
79
+ report.nextSources.forEach((source) => console.log(`- ${source}`));
80
+ }
81
+ console.log('');
82
+ }
83
+
84
+ async function loopAtris(args = []) {
85
+ const dryRun = args.includes('--dry-run');
86
+ const json = args.includes('--json');
87
+ const limitArg = args.find((arg) => arg.startsWith('--limit='));
88
+ const limit = limitArg ? Math.max(1, parseInt(limitArg.split('=')[1], 10) || 3) : 3;
89
+
90
+ const report = buildReport(process.cwd(), limit);
91
+
92
+ if (!dryRun) {
93
+ writeWikiStatus(process.cwd(), report);
94
+ appendWikiLog(
95
+ process.cwd(),
96
+ `${report.pageCount} pages, ${report.stalePages.length} stale, ${report.orphanPages.length} orphan, ${report.nextSources.length} suggested`,
97
+ report.stalePages.slice(0, 3).map((item) => `stale ${item.page} <- ${item.staleSource}`)
98
+ .concat(report.orphanPages.slice(0, 3).map((item) => `orphan ${item}`))
99
+ .concat(report.nextSources.slice(0, 3).map((item) => `next ingest ${item}`))
100
+ );
101
+ }
102
+
103
+ if (json) {
104
+ console.log(JSON.stringify(report, null, 2));
105
+ return;
106
+ }
107
+
108
+ printReport(report);
109
+ }
110
+
111
+ module.exports = {
112
+ loopAtris,
113
+ buildReport,
114
+ };