atris 3.15.0 → 3.15.12
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/AGENTS.md +35 -4
- package/README.md +3 -3
- package/atris/atris.md +38 -13
- package/atris/features/company-brain-sync/build.md +140 -0
- package/atris/features/company-brain-sync/idea.md +52 -0
- package/atris/features/company-brain-sync/validate.md +250 -0
- package/atris/skills/imessage/SKILL.md +44 -0
- package/bin/atris.js +44 -4
- package/commands/align.js +1 -1
- package/commands/brain.js +840 -0
- package/commands/business-sync.js +716 -0
- package/commands/init.js +15 -3
- package/commands/integrations.js +136 -0
- package/commands/live.js +28 -6
- package/commands/now.js +263 -0
- package/commands/pull.js +142 -8
- package/commands/push.js +181 -57
- package/commands/task.js +1658 -18
- package/commands/wiki.js +40 -1
- package/lib/company-brain-sync.js +178 -0
- package/lib/manifest.js +2 -1
- package/lib/task-db.js +271 -4
- package/lib/todo-fallback.js +34 -9
- package/lib/wiki.js +84 -4
- package/package.json +12 -2
package/commands/init.js
CHANGED
|
@@ -423,6 +423,13 @@ function initAtris() {
|
|
|
423
423
|
console.log('✓ Created TODO.md placeholder');
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
const nowFile = path.join(targetDir, 'now.md');
|
|
427
|
+
if (!fs.existsSync(nowFile)) {
|
|
428
|
+
const { renderDefaultNow } = require('./now');
|
|
429
|
+
fs.writeFileSync(nowFile, renderDefaultNow(cwd), 'utf8');
|
|
430
|
+
console.log('✓ Created now.md');
|
|
431
|
+
}
|
|
432
|
+
|
|
426
433
|
// Create lessons.md (feedback loop for learning across features)
|
|
427
434
|
const lessonsFile = path.join(targetDir, 'lessons.md');
|
|
428
435
|
if (!fs.existsSync(lessonsFile)) {
|
|
@@ -605,7 +612,9 @@ This is the Atris boot sequence. Show the output to the user, then respond natur
|
|
|
605
612
|
| File | Purpose |
|
|
606
613
|
|------|---------|
|
|
607
614
|
| \`atris/PERSONA.md\` | Communication style (read first) |
|
|
608
|
-
| \`atris
|
|
615
|
+
| \`atris task\` | Current tasks, claims, dialogue, proof |
|
|
616
|
+
| \`.atris/state/tasks.projection.json\` | Readable task projection for UIs/agents |
|
|
617
|
+
| \`atris/TODO.md\` | Rendered/legacy task view only |
|
|
609
618
|
| \`atris/MAP.md\` | Navigation (where is X?) |
|
|
610
619
|
|
|
611
620
|
## Workflow
|
|
@@ -621,14 +630,17 @@ CHECK → atris review (verify + cleanup)
|
|
|
621
630
|
- [ ] 3-4 sentences max per response
|
|
622
631
|
- [ ] Use ASCII visuals for planning
|
|
623
632
|
- [ ] Check MAP.md before touching code
|
|
624
|
-
- [ ]
|
|
625
|
-
- [ ]
|
|
633
|
+
- [ ] Run \`atris task list\` or \`atris task next\` before picking work
|
|
634
|
+
- [ ] Claim tasks with \`atris task claim <id> --as <agent>\`
|
|
635
|
+
- [ ] Finish tasks with proof via \`atris task finish <id> --proof "..."\`
|
|
636
|
+
- [ ] Treat \`atris/TODO.md\` as a rendered view; do not manually use it as the source of truth
|
|
626
637
|
|
|
627
638
|
## Anti-patterns
|
|
628
639
|
|
|
629
640
|
- Don't explore codebase manually (use MAP.md)
|
|
630
641
|
- Don't skip visualization step
|
|
631
642
|
- Don't leave stale tasks
|
|
643
|
+
- Don't hand-edit TODO.md for active task ownership
|
|
632
644
|
- Don't write verbose docs
|
|
633
645
|
|
|
634
646
|
---
|
package/commands/integrations.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
|
|
14
14
|
const { apiRequestJson } = require('../utils/api');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
15
19
|
|
|
16
20
|
async function getAuth() {
|
|
17
21
|
const ensured = await ensureValidCredentials(apiRequestJson);
|
|
@@ -310,6 +314,133 @@ async function slackCommand(subcommand, ...args) {
|
|
|
310
314
|
}
|
|
311
315
|
}
|
|
312
316
|
|
|
317
|
+
// ============================================================================
|
|
318
|
+
// IMESSAGE
|
|
319
|
+
// ============================================================================
|
|
320
|
+
|
|
321
|
+
function imessageDoctor() {
|
|
322
|
+
const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
|
|
323
|
+
const checks = {
|
|
324
|
+
macos: process.platform === 'darwin',
|
|
325
|
+
chat_db_exists: false,
|
|
326
|
+
chat_db_readable: false,
|
|
327
|
+
sqlite3_available: false,
|
|
328
|
+
osascript_available: false,
|
|
329
|
+
messages_automation: false,
|
|
330
|
+
};
|
|
331
|
+
const issues = [];
|
|
332
|
+
|
|
333
|
+
checks.chat_db_exists = fs.existsSync(chatDb);
|
|
334
|
+
if (checks.chat_db_exists) {
|
|
335
|
+
try {
|
|
336
|
+
fs.accessSync(chatDb, fs.constants.R_OK);
|
|
337
|
+
checks.chat_db_readable = true;
|
|
338
|
+
} catch {
|
|
339
|
+
issues.push('Messages database exists but is not readable. Grant Full Disk Access to this terminal or Atris.');
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
issues.push('Messages database not found on this Mac.');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
checks.sqlite3_available = spawnSync('sqlite3', ['--version'], { encoding: 'utf8' }).status === 0;
|
|
346
|
+
if (!checks.sqlite3_available) issues.push('sqlite3 is not available.');
|
|
347
|
+
|
|
348
|
+
checks.osascript_available = spawnSync('osascript', ['-e', 'return "ok"'], { encoding: 'utf8' }).status === 0;
|
|
349
|
+
if (!checks.osascript_available) issues.push('osascript is not available.');
|
|
350
|
+
if (checks.osascript_available) {
|
|
351
|
+
checks.messages_automation = spawnSync('osascript', ['-e', 'tell application "Messages" to count services'], {
|
|
352
|
+
encoding: 'utf8',
|
|
353
|
+
timeout: 4000,
|
|
354
|
+
}).status === 0;
|
|
355
|
+
}
|
|
356
|
+
if (!checks.messages_automation) issues.push('Messages automation permission is not available yet.');
|
|
357
|
+
|
|
358
|
+
if (!checks.macos) issues.push('Local iMessage requires macOS.');
|
|
359
|
+
|
|
360
|
+
const connected = checks.macos && checks.chat_db_exists && checks.chat_db_readable && checks.sqlite3_available && checks.osascript_available && checks.messages_automation;
|
|
361
|
+
return {
|
|
362
|
+
connected,
|
|
363
|
+
provider: 'local_imessage',
|
|
364
|
+
mode: 'local',
|
|
365
|
+
checks,
|
|
366
|
+
issues,
|
|
367
|
+
next_step: connected
|
|
368
|
+
? 'iMessage is available on this Mac.'
|
|
369
|
+
: 'Open System Settings -> Privacy & Security -> Full Disk Access and allow your terminal or Atris, then run this check again.',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function printImessageDoctor(result, json = false) {
|
|
374
|
+
if (json) {
|
|
375
|
+
console.log(JSON.stringify(result, null, 2));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
console.log('iMessage local check\n');
|
|
379
|
+
console.log(`Status: ${result.connected ? 'Connected on this Mac' : 'Needs permission or setup'}`);
|
|
380
|
+
for (const [name, ok] of Object.entries(result.checks)) {
|
|
381
|
+
console.log(` ${ok ? '✓' : '✗'} ${name.replace(/_/g, ' ')}`);
|
|
382
|
+
}
|
|
383
|
+
if (result.issues.length) {
|
|
384
|
+
console.log('\nNext:');
|
|
385
|
+
for (const issue of result.issues) console.log(` - ${issue}`);
|
|
386
|
+
console.log(` - ${result.next_step}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function imessageRecent(handle, options = {}) {
|
|
391
|
+
if (!handle) {
|
|
392
|
+
console.error('Usage: atris imessage recent <phone-or-email> [--limit 20]');
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
const doctor = imessageDoctor();
|
|
396
|
+
if (!doctor.connected) {
|
|
397
|
+
printImessageDoctor(doctor, Boolean(options.json));
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const limit = Number(options.limit || 20);
|
|
402
|
+
const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
|
|
403
|
+
const sql = `
|
|
404
|
+
SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
|
|
405
|
+
CASE m.is_from_me WHEN 1 THEN 'me' ELSE h.id END AS sender,
|
|
406
|
+
replace(replace(COALESCE(m.text,''), char(10), ' '), char(13), ' ') AS text
|
|
407
|
+
FROM message m
|
|
408
|
+
JOIN handle h ON h.rowid = m.handle_id
|
|
409
|
+
WHERE h.id = '${String(handle).replace(/'/g, "''")}'
|
|
410
|
+
ORDER BY m.date DESC
|
|
411
|
+
LIMIT ${Math.max(1, Math.min(100, limit))};
|
|
412
|
+
`;
|
|
413
|
+
const result = spawnSync('sqlite3', ['-readonly', chatDb, sql], { encoding: 'utf8' });
|
|
414
|
+
if (result.status !== 0) {
|
|
415
|
+
console.error(result.stderr || 'Failed to read Messages database.');
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
console.log(result.stdout.trim() || 'No recent messages found.');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function imessageCommand(subcommand, ...args) {
|
|
422
|
+
switch (subcommand) {
|
|
423
|
+
case 'doctor': {
|
|
424
|
+
const json = args.includes('--json');
|
|
425
|
+
const result = imessageDoctor();
|
|
426
|
+
printImessageDoctor(result, json);
|
|
427
|
+
if (!result.connected && args.includes('--strict')) process.exit(1);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case 'recent': {
|
|
431
|
+
const handle = args[0];
|
|
432
|
+
const limitFlag = args.findIndex((x) => x === '--limit');
|
|
433
|
+
const limit = limitFlag >= 0 ? args[limitFlag + 1] : 20;
|
|
434
|
+
imessageRecent(handle, { limit, json: args.includes('--json') });
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
default:
|
|
438
|
+
console.log('iMessage commands:');
|
|
439
|
+
console.log(' atris imessage doctor [--json] - Check local Messages access');
|
|
440
|
+
console.log(' atris imessage recent <handle> - Read recent local messages');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
313
444
|
// ============================================================================
|
|
314
445
|
// STATUS
|
|
315
446
|
// ============================================================================
|
|
@@ -337,6 +468,9 @@ async function integrationsStatus() {
|
|
|
337
468
|
}
|
|
338
469
|
}
|
|
339
470
|
|
|
471
|
+
const imessage = imessageDoctor();
|
|
472
|
+
console.log(` ${imessage.connected ? '✅' : '❌'} iMessage (local Mac)`);
|
|
473
|
+
|
|
340
474
|
console.log('\nConnect integrations at: https://atris.ai/dashboard/settings');
|
|
341
475
|
}
|
|
342
476
|
|
|
@@ -345,5 +479,7 @@ module.exports = {
|
|
|
345
479
|
calendarCommand,
|
|
346
480
|
twitterCommand,
|
|
347
481
|
slackCommand,
|
|
482
|
+
imessageCommand,
|
|
483
|
+
imessageDoctor,
|
|
348
484
|
integrationsStatus,
|
|
349
485
|
};
|
package/commands/live.js
CHANGED
|
@@ -45,21 +45,43 @@ function firstPositionalArg(args) {
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
48
|
+
function readBusinessMetaFromCwd(cwd) {
|
|
49
49
|
const file = path.join(cwd, '.atris', 'business.json');
|
|
50
50
|
if (!fs.existsSync(file)) return null;
|
|
51
51
|
try {
|
|
52
|
-
|
|
53
|
-
return data.slug || data.canonical_slug || data.name || null;
|
|
52
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
54
53
|
} catch {
|
|
55
54
|
return null;
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
58
|
+
function readBusinessSlugFromCwd(cwd) {
|
|
59
|
+
const data = readBusinessMetaFromCwd(cwd);
|
|
60
|
+
return data ? data.slug || data.canonical_slug || data.name || null : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveWorkspaceCwd(slug, cwd) {
|
|
64
|
+
if (!slug) return cwd;
|
|
65
|
+
|
|
66
|
+
const currentSlug = readBusinessSlugFromCwd(cwd);
|
|
67
|
+
if (currentSlug === slug) return cwd;
|
|
68
|
+
|
|
69
|
+
const child = path.join(cwd, slug);
|
|
70
|
+
if (fs.existsSync(child) && fs.statSync(child).isDirectory()) {
|
|
71
|
+
const childSlug = readBusinessSlugFromCwd(child);
|
|
72
|
+
const hasAtris = fs.existsSync(path.join(child, 'atris'));
|
|
73
|
+
if (childSlug || hasAtris) return child;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return cwd;
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
function parseLiveOptions(args, cwd = process.cwd()) {
|
|
60
80
|
const first = firstPositionalArg(args);
|
|
81
|
+
const slug = first || readBusinessSlugFromCwd(cwd);
|
|
82
|
+
const workspaceCwd = resolveWorkspaceCwd(slug, cwd);
|
|
61
83
|
return {
|
|
62
|
-
slug
|
|
84
|
+
slug,
|
|
63
85
|
once: args.includes('--once'),
|
|
64
86
|
dryRun: args.includes('--dry-run'),
|
|
65
87
|
noDoctor: args.includes('--no-doctor'),
|
|
@@ -68,8 +90,8 @@ function parseLiveOptions(args, cwd = process.cwd()) {
|
|
|
68
90
|
debounceSec: parseNumberFlag(args, 'debounce', DEFAULT_DEBOUNCE_SEC),
|
|
69
91
|
timeoutSec: parseNumberFlag(args, 'timeout', DEFAULT_TIMEOUT_SEC),
|
|
70
92
|
only: parseStringFlag(args, 'only'),
|
|
71
|
-
root: parseStringFlag(args, 'root') || path.dirname(
|
|
72
|
-
cwd,
|
|
93
|
+
root: parseStringFlag(args, 'root') || path.dirname(workspaceCwd),
|
|
94
|
+
cwd: workspaceCwd,
|
|
73
95
|
};
|
|
74
96
|
}
|
|
75
97
|
|
package/commands/now.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const NOW_PATH = path.join('atris', 'now.md');
|
|
5
|
+
|
|
6
|
+
function todayIso() {
|
|
7
|
+
return new Date().toISOString().split('T')[0];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ensureAtrisDir(root = process.cwd()) {
|
|
11
|
+
const atrisDir = path.join(root, 'atris');
|
|
12
|
+
if (!fs.existsSync(atrisDir)) {
|
|
13
|
+
throw new Error('atris/ folder not found. Run "atris init" first.');
|
|
14
|
+
}
|
|
15
|
+
return atrisDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasWorkspaceMarkers(atrisDir) {
|
|
19
|
+
return fs.existsSync(path.join(atrisDir, 'MAP.md')) || fs.existsSync(path.join(atrisDir, 'TODO.md'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findChildWorkspaces(root = process.cwd()) {
|
|
23
|
+
if (!fs.existsSync(root)) return [];
|
|
24
|
+
|
|
25
|
+
return fs
|
|
26
|
+
.readdirSync(root, { withFileTypes: true })
|
|
27
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
28
|
+
.map((entry) => {
|
|
29
|
+
const workspaceRoot = path.join(root, entry.name);
|
|
30
|
+
const atrisDir = path.join(workspaceRoot, 'atris');
|
|
31
|
+
if (!fs.existsSync(atrisDir) || !hasWorkspaceMarkers(atrisDir)) return null;
|
|
32
|
+
const mapPath = path.join(atrisDir, 'MAP.md');
|
|
33
|
+
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
34
|
+
return {
|
|
35
|
+
slug: entry.name,
|
|
36
|
+
root: workspaceRoot,
|
|
37
|
+
atrisDir,
|
|
38
|
+
mapPath,
|
|
39
|
+
todoPath,
|
|
40
|
+
nowPath: path.join(atrisDir, 'now.md'),
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readFirstHeading(filePath) {
|
|
48
|
+
if (!fs.existsSync(filePath)) return null;
|
|
49
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
50
|
+
const line = content.split(/\r?\n/).find((l) => l.trim().startsWith('#'));
|
|
51
|
+
return line ? line.replace(/^#+\s*/, '').trim() : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function countMatches(filePath, pattern) {
|
|
55
|
+
if (!fs.existsSync(filePath)) return 0;
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
return (content.match(pattern) || []).length;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function currentJournalPath(root = process.cwd()) {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const year = String(now.getFullYear());
|
|
63
|
+
const date = todayIso();
|
|
64
|
+
return path.join(root, 'atris', 'logs', year, `${date}.md`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderDefaultNow(root = process.cwd()) {
|
|
68
|
+
const atrisDir = ensureAtrisDir(root);
|
|
69
|
+
const mapHeading = readFirstHeading(path.join(atrisDir, 'MAP.md')) || 'MAP not filled yet';
|
|
70
|
+
const todoPath = path.join(atrisDir, 'TODO.md');
|
|
71
|
+
const journalPath = currentJournalPath(root);
|
|
72
|
+
const backlogCount = countMatches(todoPath, /^-\s+\*\*.+?\*\*/gm);
|
|
73
|
+
const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
|
|
74
|
+
const completedCount = countMatches(journalPath, /^-\s+\*\*C\d+:/gm);
|
|
75
|
+
const generated = todayIso();
|
|
76
|
+
|
|
77
|
+
return `# now
|
|
78
|
+
|
|
79
|
+
> Current operating truth for this workspace.
|
|
80
|
+
> Read this first. Follow links only when needed.
|
|
81
|
+
|
|
82
|
+
Last updated: ${generated}
|
|
83
|
+
|
|
84
|
+
## What Matters Now
|
|
85
|
+
|
|
86
|
+
- Decide the next useful move before opening more context.
|
|
87
|
+
|
|
88
|
+
## Current Priority
|
|
89
|
+
|
|
90
|
+
- Keep the workspace coherent and useful for the next human or agent.
|
|
91
|
+
|
|
92
|
+
## Signals
|
|
93
|
+
|
|
94
|
+
- Map: ${mapHeading}
|
|
95
|
+
- TODO items visible: ${backlogCount}
|
|
96
|
+
- Inbox items today: ${inboxCount}
|
|
97
|
+
- Completed receipts today: ${completedCount}
|
|
98
|
+
|
|
99
|
+
## Watchouts
|
|
100
|
+
|
|
101
|
+
- Do not treat old logs as current truth unless this file links to them.
|
|
102
|
+
- Do not create motion for its own sake.
|
|
103
|
+
- If facts conflict, surface the conflict and cite the receipts.
|
|
104
|
+
|
|
105
|
+
## Next Move
|
|
106
|
+
|
|
107
|
+
- Read \`atris/MAP.md\`, \`atris/TODO.md\`, and today's journal only as needed for the task in front of you.
|
|
108
|
+
|
|
109
|
+
## Receipts
|
|
110
|
+
|
|
111
|
+
- \`atris/MAP.md\`
|
|
112
|
+
- \`atris/TODO.md\`
|
|
113
|
+
- \`${path.relative(root, journalPath)}\`
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderPortfolioNow(root = process.cwd()) {
|
|
118
|
+
const workspaces = findChildWorkspaces(root);
|
|
119
|
+
if (workspaces.length === 0) {
|
|
120
|
+
throw new Error('atris/ folder not found. Run "atris init" first.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const generated = todayIso();
|
|
124
|
+
const lines = workspaces.map((workspace) => {
|
|
125
|
+
const heading = readFirstHeading(workspace.mapPath) || workspace.slug;
|
|
126
|
+
const todoCount = countMatches(workspace.todoPath, /^-\s+\*\*.+?\*\*/gm);
|
|
127
|
+
const nowState = fs.existsSync(workspace.nowPath) ? 'has now.md' : 'needs now.md';
|
|
128
|
+
return `- ${workspace.slug}: ${heading}; ${todoCount} visible TODO item${todoCount === 1 ? '' : 's'}; ${nowState}.`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return `# now
|
|
132
|
+
|
|
133
|
+
> Current operating truth for this portfolio of Atris workspaces.
|
|
134
|
+
> Read this first. Then enter the specific workspace that matters.
|
|
135
|
+
|
|
136
|
+
Last updated: ${generated}
|
|
137
|
+
|
|
138
|
+
## What Matters Now
|
|
139
|
+
|
|
140
|
+
- Keep the active business workspaces easy to scan, update, and hand off.
|
|
141
|
+
|
|
142
|
+
## Current Priority
|
|
143
|
+
|
|
144
|
+
- Use the child workspace with the right slug; avoid creating duplicate business brains.
|
|
145
|
+
|
|
146
|
+
## Workspace Signals
|
|
147
|
+
|
|
148
|
+
${lines.join('\n')}
|
|
149
|
+
|
|
150
|
+
## Watchouts
|
|
151
|
+
|
|
152
|
+
- Parent status is a map, not the source of truth for each business.
|
|
153
|
+
- Each active workspace should own its own \`atris/now.md\`.
|
|
154
|
+
- If slugs conflict, resolve the workspace identity before pushing or pulling.
|
|
155
|
+
|
|
156
|
+
## Next Move
|
|
157
|
+
|
|
158
|
+
- Run \`atris now\` inside the workspace you are about to operate.
|
|
159
|
+
|
|
160
|
+
## Receipts
|
|
161
|
+
|
|
162
|
+
${workspaces.map((workspace) => `- \`${workspace.slug}/atris/MAP.md\``).join('\n')}
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureNowFile(root = process.cwd()) {
|
|
167
|
+
let atrisDir = path.join(root, 'atris');
|
|
168
|
+
const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
|
|
169
|
+
const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
|
|
170
|
+
if (!isWorkspace && childWorkspaces.length === 0) {
|
|
171
|
+
ensureAtrisDir(root);
|
|
172
|
+
}
|
|
173
|
+
if (!isWorkspace && childWorkspaces.length > 0) {
|
|
174
|
+
fs.mkdirSync(atrisDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
const nowPath = path.join(atrisDir, 'now.md');
|
|
177
|
+
if (!fs.existsSync(nowPath)) {
|
|
178
|
+
const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
|
|
179
|
+
fs.writeFileSync(nowPath, content, 'utf8');
|
|
180
|
+
return { created: true, path: nowPath };
|
|
181
|
+
}
|
|
182
|
+
return { created: false, path: nowPath };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function refreshNowFile(root = process.cwd()) {
|
|
186
|
+
const atrisDir = path.join(root, 'atris');
|
|
187
|
+
const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
|
|
188
|
+
const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
|
|
189
|
+
if (!isWorkspace && childWorkspaces.length === 0) {
|
|
190
|
+
ensureAtrisDir(root);
|
|
191
|
+
}
|
|
192
|
+
if (!isWorkspace && childWorkspaces.length > 0) {
|
|
193
|
+
fs.mkdirSync(atrisDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
const nowPath = path.join(atrisDir, 'now.md');
|
|
196
|
+
const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
|
|
197
|
+
fs.writeFileSync(nowPath, content, 'utf8');
|
|
198
|
+
return { path: nowPath };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
|
|
202
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
203
|
+
if (help) {
|
|
204
|
+
console.log('Usage: atris now [--init|--refresh|--all|--path]');
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log('Show the current operating truth for this workspace.');
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' atris now Show atris/now.md');
|
|
209
|
+
console.log(' atris now --init Create atris/now.md if missing');
|
|
210
|
+
console.log(' atris now --refresh Regenerate a small local now.md');
|
|
211
|
+
console.log(' atris now --all Refresh this parent and every child Atris workspace');
|
|
212
|
+
console.log(' atris now --path Print the file path only');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const init = args.includes('--init');
|
|
217
|
+
const refresh = args.includes('--refresh');
|
|
218
|
+
const all = args.includes('--all');
|
|
219
|
+
const pathOnly = args.includes('--path');
|
|
220
|
+
|
|
221
|
+
let result;
|
|
222
|
+
if (all) {
|
|
223
|
+
const workspaces = findChildWorkspaces(root);
|
|
224
|
+
for (const workspace of workspaces) {
|
|
225
|
+
refreshNowFile(workspace.root);
|
|
226
|
+
}
|
|
227
|
+
result = refreshNowFile(root);
|
|
228
|
+
if (!pathOnly) {
|
|
229
|
+
console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
|
|
230
|
+
console.log('');
|
|
231
|
+
}
|
|
232
|
+
} else if (refresh) {
|
|
233
|
+
result = refreshNowFile(root);
|
|
234
|
+
} else if (init) {
|
|
235
|
+
result = ensureNowFile(root);
|
|
236
|
+
} else {
|
|
237
|
+
result = ensureNowFile(root);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rel = path.relative(root, result.path);
|
|
241
|
+
if (pathOnly) {
|
|
242
|
+
console.log(rel);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (result.created) {
|
|
247
|
+
console.log(`Created ${rel}`);
|
|
248
|
+
console.log('');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const content = fs.readFileSync(result.path, 'utf8').trimEnd();
|
|
252
|
+
console.log(content);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
NOW_PATH,
|
|
257
|
+
ensureNowFile,
|
|
258
|
+
findChildWorkspaces,
|
|
259
|
+
nowAtris,
|
|
260
|
+
refreshNowFile,
|
|
261
|
+
renderDefaultNow,
|
|
262
|
+
renderPortfolioNow,
|
|
263
|
+
};
|