atris 1.4.5 → 1.5.1

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.
@@ -54,6 +54,16 @@ atris activate
54
54
 
55
55
  This shows today's journal, MAP.md, and TASK_CONTEXTS.md so you can browse and take notes offline. Authentication and agent selection are only required when you want to use `atris chat` with Atris cloud agents.
56
56
 
57
+ ## Try the autopilot loop (optional)
58
+
59
+ Need a guided work session? Run:
60
+
61
+ ```bash
62
+ atris autopilot
63
+ ```
64
+
65
+ You'll pick a vision (from today's Inbox or a fresh idea), define success criteria, and then step through plan → do → review cycles. The CLI logs each iteration, and you can type `exit` at any prompt to stop.
66
+
57
67
  ## What Each File Does
58
68
 
59
69
  ### MAP.md
package/README.md CHANGED
@@ -45,6 +45,16 @@ atris login # authenticate once for cloud sync + chat
45
45
  atris chat # open an interactive session
46
46
  ```
47
47
 
48
+ ## Autopilot (beta)
49
+
50
+ Guide the whole loop with one command:
51
+
52
+ ```bash
53
+ atris autopilot
54
+ ```
55
+
56
+ Pick a vision (Inbox item or fresh idea), define the success criteria, and the CLI will walk you through plan → do → review cycles until the validator signs off. Everything is logged back to today's journal; type `exit` at any prompt to bail out.
57
+
48
58
  ---
49
59
 
50
60
  **License:** MIT | **Repo:** [github.com/atrislabs/atris.md](https://github.com/atrislabs/atris.md.git)
@@ -105,6 +105,16 @@ Once the files are populated, you can interact with your agents:
105
105
  @validator check if the recent auth changes are safe to merge
106
106
  ```
107
107
 
108
+ ## Try the autopilot loop (optional)
109
+
110
+ Need a guided work session? Run:
111
+
112
+ ```bash
113
+ atris autopilot
114
+ ```
115
+
116
+ Pick a vision (today's Inbox or a fresh idea), set success criteria, and follow the guided plan → do → review cycles. Each iteration gets logged, and you can type `exit` at any prompt to stop.
117
+
108
118
  ## Keeping ATRIS Updated
109
119
 
110
120
  When the ATRIS package updates with new features:
package/bin/atris.js CHANGED
@@ -11,6 +11,43 @@ const crypto = require('crypto');
11
11
 
12
12
  const command = process.argv[2];
13
13
 
14
+ const TOKEN_REFRESH_BUFFER_SECONDS = 300; // Refresh ~5 minutes before expiry
15
+
16
+ function decodeJwtClaims(token) {
17
+ if (!token || typeof token !== 'string') {
18
+ return null;
19
+ }
20
+ const parts = token.split('.');
21
+ if (parts.length < 2) {
22
+ return null;
23
+ }
24
+ try {
25
+ const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');
26
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');
27
+ const decoded = Buffer.from(padded, 'base64').toString('utf8');
28
+ return JSON.parse(decoded);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function getTokenExpiryEpochSeconds(token) {
35
+ const claims = decodeJwtClaims(token);
36
+ if (!claims || typeof claims.exp !== 'number') {
37
+ return null;
38
+ }
39
+ return claims.exp;
40
+ }
41
+
42
+ function shouldRefreshToken(token, bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS) {
43
+ const exp = getTokenExpiryEpochSeconds(token);
44
+ if (!exp) {
45
+ return false;
46
+ }
47
+ const nowSeconds = Math.floor(Date.now() / 1000);
48
+ return exp <= nowSeconds + bufferSeconds;
49
+ }
50
+
14
51
  function showHelp() {
15
52
  console.log('Usage: atris <command>');
16
53
  console.log('Commands:');
@@ -23,6 +60,7 @@ function showHelp() {
23
60
  console.log(' do - Activate executor (build tasks from TASK_CONTEXTS)');
24
61
  console.log(' review - Activate validator (verify, test, clean docs)');
25
62
  console.log(' chat - Interactive chat with ATRIS agents');
63
+ console.log(' autopilot - Guided plan → do → review loop with success criteria');
26
64
  console.log(' visualize - Break down ideas from inbox with 3-4 sentences + ASCII diagram');
27
65
  console.log(' log - View or append to today\'s log');
28
66
  console.log(' log sync - Sync today\'s log to Atris journal');
@@ -83,6 +121,16 @@ if (command === 'init') {
83
121
  doAtris();
84
122
  } else if (command === 'review') {
85
123
  reviewAtris();
124
+ } else if (command === 'autopilot') {
125
+ autopilotAtris()
126
+ .then(() => process.exit(0))
127
+ .catch((error) => {
128
+ if (error && error.__autopilotAbort) {
129
+ process.exit(0);
130
+ }
131
+ console.error(`✗ Autopilot failed: ${error.message || error}`);
132
+ process.exit(1);
133
+ });
86
134
  } else if (command === 'status') {
87
135
  statusAtris();
88
136
  } else if (command === 'analytics') {
@@ -258,7 +306,9 @@ function getLogPath(dateStr) {
258
306
  const targetDir = path.join(process.cwd(), 'atris');
259
307
  const date = dateStr ? new Date(dateStr) : new Date();
260
308
  const year = date.getFullYear();
261
- const dateFormatted = date.toISOString().split('T')[0]; // YYYY-MM-DD
309
+ const month = String(date.getMonth() + 1).padStart(2, '0');
310
+ const day = String(date.getDate()).padStart(2, '0');
311
+ const dateFormatted = `${year}-${month}-${day}`; // YYYY-MM-DD in local time
262
312
 
263
313
  const logsDir = path.join(targetDir, 'logs');
264
314
  const yearDir = path.join(logsDir, year.toString());
@@ -461,41 +511,96 @@ async function logSyncAtris() {
461
511
  return;
462
512
  }
463
513
 
464
- // Remote is newer - prompt user
465
- console.log('⚠️ Web version is newer than local version');
466
- console.log(` Remote updated: ${remoteUpdatedAt}`);
467
- console.log(` Local modified: ${localModified}`);
468
- console.log(' Type "y" to replace your local file with the web version, or "n" to keep local changes and push them to the web.');
469
- console.log('');
514
+ // Try section-based merge
515
+ try {
516
+ const localSections = parseJournalSections(normalizedLocal);
517
+ const remoteSections = parseJournalSections(normalizedRemote || '');
518
+ const { merged, conflicts } = mergeSections(localSections, remoteSections, knownRemoteHash);
519
+
520
+ if (conflicts.length === 0) {
521
+ // Clean merge - auto-merge and continue
522
+ const mergedContent = reconstructJournal(merged);
523
+ fs.writeFileSync(logFile, mergedContent, 'utf8');
524
+ console.log('✓ Auto-merged web and local changes');
525
+ console.log(` Merged sections: ${Object.keys(merged).filter(k => k !== '__header__').join(', ')}`);
526
+ // Update local content for push
527
+ localContent = mergedContent;
528
+ } else {
529
+ // Conflicts detected - prompt user
530
+ console.log('⚠️ Conflicting changes in same section(s)');
531
+ console.log(` Conflicts: ${conflicts.join(', ')}`);
532
+ console.log(` Remote updated: ${remoteUpdatedAt}`);
533
+ console.log(` Local modified: ${localModified}`);
534
+ console.log(' Type "y" to replace local with web version, or "n" to keep local changes.');
535
+ console.log('');
536
+
537
+ if (typeof remoteContent === 'string') {
538
+ showLogDiff(logFile, remoteContent);
539
+ }
470
540
 
471
- if (typeof remoteContent === 'string') {
472
- showLogDiff(logFile, remoteContent);
473
- }
541
+ const answer = await promptUser('Overwrite local with web version? (y/n): ');
542
+
543
+ if (answer && answer.toLowerCase() === 'y') {
544
+ // Pull remote content
545
+ const pulledContent = existing.data?.content || '';
546
+ fs.writeFileSync(logFile, pulledContent, 'utf8');
547
+ remoteHash = computeContentHash(pulledContent);
548
+ console.log('✓ Local journal updated from web');
549
+ console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
550
+ if (remoteUpdatedAt) {
551
+ const remoteDate = new Date(remoteUpdatedAt);
552
+ if (!Number.isNaN(remoteDate.getTime())) {
553
+ fs.utimesSync(logFile, remoteDate, remoteDate);
554
+ }
555
+ const state = loadLogSyncState();
556
+ state[dateFormatted] = {
557
+ updated_at: remoteUpdatedAt,
558
+ hash: remoteHash || computeContentHash(pulledContent),
559
+ };
560
+ saveLogSyncState(state);
561
+ }
562
+ return;
563
+ } else {
564
+ console.log('⏩ Keeping local version, will push to web');
565
+ }
566
+ }
567
+ } catch (parseError) {
568
+ // Fallback to old prompt behavior if parsing fails
569
+ console.log('⚠️ Web version is newer than local version');
570
+ console.log(` Remote updated: ${remoteUpdatedAt}`);
571
+ console.log(` Local modified: ${localModified}`);
572
+ console.log(' Type "y" to replace your local file with the web version, or "n" to keep local changes and push them to the web.');
573
+ console.log('');
574
+
575
+ if (typeof remoteContent === 'string') {
576
+ showLogDiff(logFile, remoteContent);
577
+ }
474
578
 
475
- const answer = await promptUser('Overwrite local with web version? (y/n): ');
476
-
477
- if (answer && answer.toLowerCase() === 'y') {
478
- // Pull remote content
479
- const pulledContent = existing.data?.content || '';
480
- fs.writeFileSync(logFile, pulledContent, 'utf8');
481
- remoteHash = computeContentHash(pulledContent);
482
- console.log('✓ Local journal updated from web');
483
- console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
484
- if (remoteUpdatedAt) {
485
- const remoteDate = new Date(remoteUpdatedAt);
486
- if (!Number.isNaN(remoteDate.getTime())) {
487
- fs.utimesSync(logFile, remoteDate, remoteDate);
579
+ const answer = await promptUser('Overwrite local with web version? (y/n): ');
580
+
581
+ if (answer && answer.toLowerCase() === 'y') {
582
+ // Pull remote content
583
+ const pulledContent = existing.data?.content || '';
584
+ fs.writeFileSync(logFile, pulledContent, 'utf8');
585
+ remoteHash = computeContentHash(pulledContent);
586
+ console.log('✓ Local journal updated from web');
587
+ console.log(`🗒️ File: ${path.relative(process.cwd(), logFile)}`);
588
+ if (remoteUpdatedAt) {
589
+ const remoteDate = new Date(remoteUpdatedAt);
590
+ if (!Number.isNaN(remoteDate.getTime())) {
591
+ fs.utimesSync(logFile, remoteDate, remoteDate);
592
+ }
593
+ const state = loadLogSyncState();
594
+ state[dateFormatted] = {
595
+ updated_at: remoteUpdatedAt,
596
+ hash: remoteHash || computeContentHash(pulledContent),
597
+ };
598
+ saveLogSyncState(state);
488
599
  }
489
- const state = loadLogSyncState();
490
- state[dateFormatted] = {
491
- updated_at: remoteUpdatedAt,
492
- hash: remoteHash || computeContentHash(pulledContent),
493
- };
494
- saveLogSyncState(state);
600
+ return;
601
+ } else {
602
+ console.log('⏩ Keeping local version, will push to web');
495
603
  }
496
- return;
497
- } else {
498
- console.log('⏩ Keeping local version, will push to web');
499
604
  }
500
605
  } else if (remoteTime > localTime && remoteMatchesKnown) {
501
606
  console.log('⚠️ Web timestamp ahead due to clock skew (matches last sync); pushing local changes.');
@@ -562,8 +667,6 @@ async function logSyncAtris() {
562
667
  hash: finalHash,
563
668
  };
564
669
  saveLogSyncState(finalState);
565
- console.log('');
566
- console.log('Next: open the dashboard journal to verify formatting, or run this command after edits to stay in sync.');
567
670
  }
568
671
 
569
672
  function showTodayLog() {
@@ -814,12 +917,74 @@ async function refreshAccessToken(refreshToken, provider) {
814
917
  });
815
918
  }
816
919
 
920
+ async function performTokenRefresh(credentials, sourceLabel = 'refreshed') {
921
+ if (!credentials || !credentials.refresh_token) {
922
+ return { ok: false, error: 'missing_refresh_token' };
923
+ }
924
+
925
+ const refreshed = await refreshAccessToken(credentials.refresh_token, credentials.provider);
926
+ if (!refreshed.ok) {
927
+ return { ok: false, error: refreshed.error || 'Refresh request failed' };
928
+ }
929
+
930
+ const accessToken = refreshed.data?.access_token;
931
+ if (!accessToken) {
932
+ return { ok: false, error: 'No access token returned by refresh API' };
933
+ }
934
+
935
+ const newRefreshToken = refreshed.data?.refresh_token || credentials.refresh_token;
936
+ const refreshUser = refreshed.data?.user || null;
937
+ const provider = refreshed.data?.provider || credentials.provider;
938
+ const email = refreshUser?.email || credentials.email;
939
+ const userId = refreshUser?.id || credentials.user_id;
940
+
941
+ saveCredentials(accessToken, newRefreshToken, email, userId, provider);
942
+ let latestCreds = loadCredentials();
943
+
944
+ const validation = await validateAccessToken(accessToken);
945
+ let finalUser = refreshUser;
946
+
947
+ if (validation.ok && validation.data?.valid) {
948
+ finalUser = validation.data.user || refreshUser || null;
949
+ const updatedEmail = finalUser?.email || latestCreds?.email || email;
950
+ const updatedProvider = finalUser?.provider || latestCreds?.provider || provider;
951
+ const updatedUserId = finalUser?.id || latestCreds?.user_id || userId;
952
+
953
+ if (
954
+ !latestCreds ||
955
+ updatedEmail !== latestCreds.email ||
956
+ updatedProvider !== latestCreds.provider ||
957
+ updatedUserId !== latestCreds.user_id
958
+ ) {
959
+ saveCredentials(accessToken, newRefreshToken, updatedEmail, updatedUserId, updatedProvider);
960
+ latestCreds = loadCredentials();
961
+ }
962
+ }
963
+
964
+ return {
965
+ ok: true,
966
+ payload: {
967
+ credentials: latestCreds || loadCredentials(),
968
+ user: finalUser,
969
+ source: sourceLabel,
970
+ },
971
+ };
972
+ }
973
+
817
974
  async function ensureValidCredentials(options = {}) {
818
- const credentials = loadCredentials();
975
+ let credentials = loadCredentials();
819
976
  if (!credentials || !credentials.token) {
820
977
  return { error: 'not_logged_in' };
821
978
  }
822
979
 
980
+ if (credentials.refresh_token && shouldRefreshToken(credentials.token)) {
981
+ const proactive = await performTokenRefresh(credentials, 'proactive_refresh');
982
+ if (proactive.ok) {
983
+ return proactive.payload;
984
+ }
985
+ credentials = loadCredentials() || credentials;
986
+ }
987
+
823
988
  const validation = await validateAccessToken(credentials.token);
824
989
  if (validation.ok && validation.data?.valid) {
825
990
  const user = validation.data.user || null;
@@ -852,34 +1017,12 @@ async function ensureValidCredentials(options = {}) {
852
1017
  return { error: 'token_invalid', detail: validation.error || 'Token expired' };
853
1018
  }
854
1019
 
855
- const refreshed = await refreshAccessToken(credentials.refresh_token, credentials.provider);
1020
+ const refreshed = await performTokenRefresh(credentials, 'refreshed');
856
1021
  if (!refreshed.ok) {
857
1022
  return { error: 'refresh_failed', detail: refreshed.error };
858
1023
  }
859
1024
 
860
- const accessToken = refreshed.data?.access_token;
861
- const newRefreshToken = refreshed.data?.refresh_token || credentials.refresh_token;
862
- const refreshUser = refreshed.data?.user || null;
863
- const provider = refreshed.data?.provider || credentials.provider;
864
- const email = refreshUser?.email || credentials.email;
865
- const userId = refreshUser?.id || credentials.user_id;
866
-
867
- if (!accessToken) {
868
- return { error: 'refresh_failed', detail: 'No access token returned by refresh API' };
869
- }
870
-
871
- saveCredentials(accessToken, newRefreshToken, email, userId, provider);
872
- const latestValidation = await validateAccessToken(accessToken);
873
- const finalUser =
874
- latestValidation.ok && latestValidation.data?.valid
875
- ? latestValidation.data.user || refreshUser
876
- : refreshUser;
877
-
878
- return {
879
- credentials: loadCredentials(),
880
- user: finalUser,
881
- source: 'refreshed',
882
- };
1025
+ return refreshed.payload;
883
1026
  }
884
1027
 
885
1028
  async function fetchMyAgents(token) {
@@ -1195,6 +1338,98 @@ function computeContentHash(content) {
1195
1338
  return crypto.createHash('sha256').update(normalized).digest('hex');
1196
1339
  }
1197
1340
 
1341
+ function parseJournalSections(content) {
1342
+ const sections = {};
1343
+ const lines = content.split('\n');
1344
+ let currentSection = '__header__';
1345
+ let currentContent = [];
1346
+
1347
+ for (const line of lines) {
1348
+ if (line.startsWith('## ')) {
1349
+ // Save previous section
1350
+ if (currentContent.length > 0 || currentSection === '__header__') {
1351
+ sections[currentSection] = currentContent.join('\n');
1352
+ }
1353
+ // Start new section
1354
+ currentSection = line.substring(3).trim();
1355
+ currentContent = [line];
1356
+ } else {
1357
+ currentContent.push(line);
1358
+ }
1359
+ }
1360
+
1361
+ // Save last section
1362
+ if (currentContent.length > 0) {
1363
+ sections[currentSection] = currentContent.join('\n');
1364
+ }
1365
+
1366
+ return sections;
1367
+ }
1368
+
1369
+ function mergeSections(localSections, remoteSections, knownRemoteHash) {
1370
+ const merged = {};
1371
+ const conflicts = [];
1372
+
1373
+ // Get all unique section names
1374
+ const allSections = new Set([...Object.keys(localSections), ...Object.keys(remoteSections)]);
1375
+
1376
+ for (const section of allSections) {
1377
+ const localContent = localSections[section] || '';
1378
+ const remoteContent = remoteSections[section] || '';
1379
+
1380
+ if (localContent === remoteContent) {
1381
+ // Same content, use either
1382
+ merged[section] = localContent;
1383
+ } else if (!remoteContent) {
1384
+ // Only in local, keep local
1385
+ merged[section] = localContent;
1386
+ } else if (!localContent) {
1387
+ // Only in remote, keep remote
1388
+ merged[section] = remoteContent;
1389
+ } else {
1390
+ // Both exist but differ - check if remote matches known state
1391
+ const remoteHash = computeContentHash(remoteContent);
1392
+ if (knownRemoteHash && remoteHash === knownRemoteHash) {
1393
+ // Remote hasn't changed since last sync, prefer local
1394
+ merged[section] = localContent;
1395
+ } else {
1396
+ // Real conflict - mark for user review
1397
+ conflicts.push(section);
1398
+ merged[section] = localContent; // Default to local
1399
+ }
1400
+ }
1401
+ }
1402
+
1403
+ return { merged, conflicts };
1404
+ }
1405
+
1406
+ function reconstructJournal(sections) {
1407
+ const parts = [];
1408
+
1409
+ // Header first
1410
+ if (sections['__header__']) {
1411
+ parts.push(sections['__header__']);
1412
+ }
1413
+
1414
+ // Then all other sections in order (preserve original order where possible)
1415
+ const sectionOrder = ['Completed ✅', 'In Progress 🔄', 'Backlog', 'Notes', 'Inbox', 'Timestamps', 'Lessons Learned'];
1416
+
1417
+ for (const section of sectionOrder) {
1418
+ if (sections[section]) {
1419
+ parts.push(sections[section]);
1420
+ }
1421
+ }
1422
+
1423
+ // Add any remaining sections not in the standard order
1424
+ for (const [section, content] of Object.entries(sections)) {
1425
+ if (section !== '__header__' && !sectionOrder.includes(section)) {
1426
+ parts.push(content);
1427
+ }
1428
+ }
1429
+
1430
+ return parts.join('\n');
1431
+ }
1432
+
1198
1433
  function showLogDiff(localPath, remoteContent) {
1199
1434
  let tmpDir;
1200
1435
  try {
@@ -1603,6 +1838,410 @@ function visualizeAtris() {
1603
1838
  console.log('');
1604
1839
  }
1605
1840
 
1841
+ async function autopilotAtris() {
1842
+ const targetDir = path.join(process.cwd(), 'atris');
1843
+ if (!fs.existsSync(targetDir)) {
1844
+ throw new Error('atris/ folder not found. Run "atris init" first.');
1845
+ }
1846
+
1847
+ const navigatorFile = path.join(targetDir, 'agent_team', 'navigator.md');
1848
+ const executorFile = path.join(targetDir, 'agent_team', 'executor.md');
1849
+ const validatorFile = path.join(targetDir, 'agent_team', 'validator.md');
1850
+
1851
+ const missingSpecs = [];
1852
+ if (!fs.existsSync(navigatorFile)) missingSpecs.push('navigator.md');
1853
+ if (!fs.existsSync(executorFile)) missingSpecs.push('executor.md');
1854
+ if (!fs.existsSync(validatorFile)) missingSpecs.push('validator.md');
1855
+
1856
+ if (missingSpecs.length > 0) {
1857
+ throw new Error(`Missing agent spec(s): ${missingSpecs.join(', ')}. Run "atris init" to restore them.`);
1858
+ }
1859
+
1860
+ ensureLogDirectory();
1861
+ const { logFile, dateFormatted } = getLogPath();
1862
+ if (!fs.existsSync(logFile)) {
1863
+ createLogFile(logFile, dateFormatted);
1864
+ }
1865
+
1866
+ console.log('');
1867
+ console.log('┌─────────────────────────────────────────────────────────────┐');
1868
+ console.log('│ ATRIS Autopilot — plan → do → review loop │');
1869
+ console.log('└─────────────────────────────────────────────────────────────┘');
1870
+ console.log('');
1871
+ console.log(`Date: ${dateFormatted}`);
1872
+ console.log('Type "exit" at any prompt to cancel.');
1873
+ console.log('');
1874
+
1875
+ const rl = readline.createInterface({
1876
+ input: process.stdin,
1877
+ output: process.stdout,
1878
+ });
1879
+
1880
+ const ask = async (promptText, options = {}) => {
1881
+ const { allowEmpty = false } = options;
1882
+ while (true) {
1883
+ const answer = await new Promise((resolve) => rl.question(promptText, resolve));
1884
+ const trimmed = answer.trim();
1885
+ if (trimmed.toLowerCase() === 'exit') {
1886
+ throw autopilotAbortError();
1887
+ }
1888
+ if (!allowEmpty && trimmed === '') {
1889
+ console.log('Please enter a value (or type "exit" to abort).');
1890
+ continue;
1891
+ }
1892
+ return trimmed;
1893
+ }
1894
+ };
1895
+
1896
+ const askYesNo = async (promptText) => {
1897
+ while (true) {
1898
+ const response = (await ask(promptText)).toLowerCase();
1899
+ if (response === 'y' || response === 'yes') return true;
1900
+ if (response === 'n' || response === 'no') return false;
1901
+ console.log('Please answer with "y" or "n" (or type "exit" to abort).');
1902
+ }
1903
+ };
1904
+
1905
+ let selectedInboxItem = null;
1906
+ let visionSummary = '';
1907
+ let sourceLabel = 'Ad-hoc';
1908
+
1909
+ try {
1910
+ const initialLogContent = fs.readFileSync(logFile, 'utf8');
1911
+ let inboxItems = parseInboxItems(initialLogContent);
1912
+
1913
+ if (inboxItems.length > 0) {
1914
+ console.log('Choose a vision source:');
1915
+ console.log(' 1. Select an item from today\'s Inbox');
1916
+ console.log(' 2. Enter a new idea');
1917
+ console.log('');
1918
+
1919
+ let choice;
1920
+ while (true) {
1921
+ choice = await ask('Choice (1-2): ');
1922
+ if (choice === '1' || choice === '2') {
1923
+ break;
1924
+ }
1925
+ console.log('Please enter 1 or 2.');
1926
+ }
1927
+
1928
+ if (choice === '1') {
1929
+ console.log('');
1930
+ console.log('Today\'s Inbox:');
1931
+ inboxItems.forEach((item, index) => {
1932
+ console.log(` ${index + 1}. I${item.id} — ${item.text}`);
1933
+ });
1934
+ console.log('');
1935
+
1936
+ while (true) {
1937
+ const selection = await ask(`Pick an item (1-${inboxItems.length}): `);
1938
+ const index = parseInt(selection, 10);
1939
+ if (!Number.isNaN(index) && index >= 1 && index <= inboxItems.length) {
1940
+ selectedInboxItem = inboxItems[index - 1];
1941
+ break;
1942
+ }
1943
+ console.log(`Enter a number between 1 and ${inboxItems.length}.`);
1944
+ }
1945
+
1946
+ const editedSummary = await ask('Vision summary (press Enter to keep original): ', { allowEmpty: true });
1947
+ visionSummary = editedSummary ? editedSummary : selectedInboxItem.text;
1948
+ } else {
1949
+ console.log('');
1950
+ visionSummary = await ask('Describe the vision: ');
1951
+ const newId = addInboxIdea(logFile, visionSummary);
1952
+ console.log(`✓ Added I${newId} to today\'s Inbox.`);
1953
+ selectedInboxItem = { id: newId, text: visionSummary };
1954
+ }
1955
+ } else {
1956
+ console.log('No items in today\'s Inbox. Capture a new idea to begin.');
1957
+ visionSummary = await ask('Describe the vision: ');
1958
+ const newId = addInboxIdea(logFile, visionSummary);
1959
+ console.log(`✓ Added I${newId} to today\'s Inbox.`);
1960
+ selectedInboxItem = { id: newId, text: visionSummary };
1961
+ }
1962
+
1963
+ sourceLabel = selectedInboxItem ? `I${selectedInboxItem.id}` : 'Ad-hoc';
1964
+
1965
+ console.log('');
1966
+ console.log('Define the success criteria (one per line, blank line to finish).');
1967
+ const successCriteria = [];
1968
+ while (true) {
1969
+ const criteria = await ask(`Success criteria ${successCriteria.length + 1}: `, {
1970
+ allowEmpty: successCriteria.length > 0,
1971
+ });
1972
+ if (!criteria) {
1973
+ if (successCriteria.length === 0) {
1974
+ console.log('Please provide at least one success criteria.');
1975
+ continue;
1976
+ }
1977
+ break;
1978
+ }
1979
+ successCriteria.push(criteria);
1980
+ }
1981
+
1982
+ const riskNotes = await ask('Any risks or notes? (optional): ', { allowEmpty: true });
1983
+
1984
+ recordAutopilotVision(
1985
+ logFile,
1986
+ sourceLabel,
1987
+ visionSummary,
1988
+ successCriteria,
1989
+ riskNotes ? riskNotes : ''
1990
+ );
1991
+
1992
+ console.log('');
1993
+ console.log('Vision locked in:');
1994
+ console.log(`• Source: ${sourceLabel}`);
1995
+ console.log(`• Summary: ${visionSummary}`);
1996
+ console.log('• Success Criteria:');
1997
+ successCriteria.forEach((item, index) => {
1998
+ console.log(` ${index + 1}. ${item}`);
1999
+ });
2000
+ if (riskNotes) {
2001
+ console.log(`• Notes: ${riskNotes}`);
2002
+ }
2003
+ console.log('');
2004
+ console.log('Starting plan → do → review cycles.');
2005
+ console.log('');
2006
+
2007
+ let iteration = 1;
2008
+ while (true) {
2009
+ console.log(`════ Iteration ${iteration} ════`);
2010
+
2011
+ console.log('\n[Plan]');
2012
+ planAtris();
2013
+ await ask('Press Enter when planning is complete: ', { allowEmpty: true });
2014
+
2015
+ console.log('\n[Do]');
2016
+ doAtris();
2017
+ await ask('Press Enter when execution is complete: ', { allowEmpty: true });
2018
+
2019
+ console.log('\n[Review]');
2020
+ reviewAtris();
2021
+ await ask('Press Enter when validation is complete: ', { allowEmpty: true });
2022
+
2023
+ const isSuccess = await askYesNo('Did we meet the success criteria? (y/n): ');
2024
+ if (isSuccess) {
2025
+ const successNotes = await ask('Notes for the log (optional): ', { allowEmpty: true });
2026
+ recordAutopilotIteration(
2027
+ logFile,
2028
+ iteration,
2029
+ 'Success',
2030
+ successNotes ? successNotes : ''
2031
+ );
2032
+ recordAutopilotSuccess(
2033
+ logFile,
2034
+ selectedInboxItem ? selectedInboxItem.id : null,
2035
+ visionSummary
2036
+ );
2037
+ console.log('\n✓ Success recorded. Autopilot complete.');
2038
+ break;
2039
+ } else {
2040
+ const followUp = await ask('Describe remaining blockers / next steps (optional): ', {
2041
+ allowEmpty: true,
2042
+ });
2043
+ recordAutopilotIteration(
2044
+ logFile,
2045
+ iteration,
2046
+ 'Follow-up required',
2047
+ followUp ? followUp : ''
2048
+ );
2049
+ const continueLoop = await askYesNo('Run another plan → do → review cycle? (y/n): ');
2050
+ if (!continueLoop) {
2051
+ console.log('\nAutopilot session ended. Success criteria not yet met.');
2052
+ break;
2053
+ }
2054
+ iteration += 1;
2055
+ }
2056
+ }
2057
+ } finally {
2058
+ rl.close();
2059
+ }
2060
+ }
2061
+
2062
+ function autopilotAbortError() {
2063
+ const error = new Error('Autopilot cancelled by user.');
2064
+ error.__autopilotAbort = true;
2065
+ return error;
2066
+ }
2067
+
2068
+ function addInboxIdea(logFile, summary) {
2069
+ const content = fs.readFileSync(logFile, 'utf8');
2070
+ const nextId = getNextInboxId(content);
2071
+ const updated = addInboxItemToContent(content, nextId, summary);
2072
+ fs.writeFileSync(logFile, updated);
2073
+ return nextId;
2074
+ }
2075
+
2076
+ function parseInboxItems(content) {
2077
+ const match = content.match(/## Inbox\n([\s\S]*?)(?=\n##|\n---|$)/);
2078
+ if (!match) {
2079
+ return [];
2080
+ }
2081
+ const body = match[1];
2082
+ const lines = body.split('\n');
2083
+ const items = [];
2084
+ lines.forEach((line) => {
2085
+ const trimmed = line.trim();
2086
+ if (!trimmed) return;
2087
+ if (trimmed.startsWith('(Empty')) return;
2088
+ const parsed = trimmed.match(/^- \*\*I(\d+):\*\*\s*(.+)$|^- \*\*I(\d+):\s+(.+)$/);
2089
+ if (parsed) {
2090
+ const id = parseInt(parsed[1] || parsed[3], 10);
2091
+ const text = parsed[2] || parsed[4];
2092
+ items.push({ id, text, line: trimmed });
2093
+ }
2094
+ });
2095
+ return items;
2096
+ }
2097
+
2098
+ function replaceInboxSection(content, items) {
2099
+ const regex = /(## Inbox\n)([\s\S]*?)(\n---|\n##|$)/;
2100
+ if (!regex.test(content)) {
2101
+ const lines = items.length ? items.map((item) => item.line).join('\n') : '(Empty - inbox zero achieved)';
2102
+ return `${content}\n\n## Inbox\n\n${lines}\n`;
2103
+ }
2104
+
2105
+ return content.replace(regex, (match, header, body, suffix) => {
2106
+ const inner = items.length
2107
+ ? `\n${items.map((item) => item.line).join('\n')}\n`
2108
+ : '\n(Empty - inbox zero achieved)\n';
2109
+ return `${header}${inner}${suffix}`;
2110
+ });
2111
+ }
2112
+
2113
+ function addInboxItemToContent(content, id, summary) {
2114
+ const items = parseInboxItems(content).filter((item) => item.id !== id);
2115
+ const newItem = { id, text: summary, line: `- **I${id}:** ${summary}` };
2116
+ const updatedItems = [newItem, ...items];
2117
+ return replaceInboxSection(content, updatedItems);
2118
+ }
2119
+
2120
+ function removeInboxItemFromContent(content, id) {
2121
+ const items = parseInboxItems(content).filter((item) => item.id !== id);
2122
+ return replaceInboxSection(content, items);
2123
+ }
2124
+
2125
+ function getNextInboxId(content) {
2126
+ const items = parseInboxItems(content);
2127
+ if (items.length === 0) return 1;
2128
+ return items.reduce((max, item) => (item.id > max ? item.id : max), 0) + 1;
2129
+ }
2130
+
2131
+ function parseCompletionItems(content) {
2132
+ const match = content.match(/## Completed ✅\n([\s\S]*?)(?=\n##|\n---|$)/);
2133
+ if (!match) {
2134
+ return [];
2135
+ }
2136
+ const body = match[1];
2137
+ const lines = body.split('\n');
2138
+ const items = [];
2139
+ lines.forEach((line) => {
2140
+ const trimmed = line.trim();
2141
+ if (!trimmed) return;
2142
+ if (trimmed.startsWith('(Empty')) return;
2143
+ const parsed = trimmed.match(/^- \*\*C(\d+):\*\*\s*(.+)$|^- \*\*C(\d+):\s+(.+)$/);
2144
+ if (parsed) {
2145
+ const id = parseInt(parsed[1] || parsed[3], 10);
2146
+ const text = parsed[2] || parsed[4];
2147
+ items.push({ id, text, line: trimmed });
2148
+ }
2149
+ });
2150
+ return items;
2151
+ }
2152
+
2153
+ function replaceCompletedSection(content, items) {
2154
+ const regex = /(## Completed ✅\n)([\s\S]*?)(\n---|\n##|$)/;
2155
+ if (!regex.test(content)) {
2156
+ const lines = items.length ? items.map((item) => item.line).join('\n') : '';
2157
+ return `${content}\n\n## Completed ✅\n\n${lines}\n`;
2158
+ }
2159
+
2160
+ return content.replace(regex, (match, header, body, suffix) => {
2161
+ const inner = items.length
2162
+ ? `\n${items.map((item) => item.line).join('\n')}\n`
2163
+ : '\n';
2164
+ return `${header}${inner}${suffix}`;
2165
+ });
2166
+ }
2167
+
2168
+ function addCompletionItemToContent(content, id, summary) {
2169
+ const items = parseCompletionItems(content).filter((item) => item.id !== id);
2170
+ const newItem = { id, text: summary, line: `- **C${id}:** ${summary}` };
2171
+ const updatedItems = [...items, newItem];
2172
+ return replaceCompletedSection(content, updatedItems);
2173
+ }
2174
+
2175
+ function getNextCompletionId(content) {
2176
+ const items = parseCompletionItems(content);
2177
+ if (items.length === 0) return 1;
2178
+ return items.reduce((max, item) => (item.id > max ? item.id : max), 0) + 1;
2179
+ }
2180
+
2181
+ function insertIntoNotesSection(content, block) {
2182
+ const regex = /(## Notes\n)([\s\S]*?)(\n---|\n##|$)/;
2183
+ const match = content.match(regex);
2184
+ if (!match) {
2185
+ return `${content}\n\n## Notes\n\n${block}\n`;
2186
+ }
2187
+ const header = match[1];
2188
+ const body = match[2];
2189
+ const suffix = match[3];
2190
+ const trimmedBody = body.replace(/\s*$/, '');
2191
+ const newBody = trimmedBody
2192
+ ? `${trimmedBody}\n\n${block}\n`
2193
+ : `\n${block}\n`;
2194
+ return content.replace(regex, `${header}${newBody}${suffix}`);
2195
+ }
2196
+
2197
+ function getTimeLabel() {
2198
+ const now = new Date();
2199
+ const hours = String(now.getHours()).padStart(2, '0');
2200
+ const minutes = String(now.getMinutes()).padStart(2, '0');
2201
+ return `${hours}:${minutes}`;
2202
+ }
2203
+
2204
+ function recordAutopilotVision(logFile, sourceLabel, summary, successCriteria, riskNotes) {
2205
+ const content = fs.readFileSync(logFile, 'utf8');
2206
+ const lines = [
2207
+ `### Autopilot Vision — ${getTimeLabel()}`,
2208
+ `**Source:** ${sourceLabel}`,
2209
+ `**Summary:** ${summary}`,
2210
+ '**Success Criteria:**',
2211
+ ...successCriteria.map((item) => `- ${item}`),
2212
+ ];
2213
+ if (riskNotes && riskNotes.trim()) {
2214
+ lines.push(`**Risks / Notes:** ${riskNotes}`);
2215
+ }
2216
+ const block = lines.join('\n');
2217
+ const updated = insertIntoNotesSection(content, block);
2218
+ fs.writeFileSync(logFile, updated);
2219
+ }
2220
+
2221
+ function recordAutopilotIteration(logFile, iteration, result, notes) {
2222
+ const content = fs.readFileSync(logFile, 'utf8');
2223
+ const lines = [
2224
+ `### Autopilot Iteration ${iteration} — ${getTimeLabel()}`,
2225
+ `**Validator Result:** ${result}`,
2226
+ ];
2227
+ if (notes && notes.trim()) {
2228
+ lines.push(`**Notes:** ${notes}`);
2229
+ }
2230
+ const block = lines.join('\n');
2231
+ const updated = insertIntoNotesSection(content, block);
2232
+ fs.writeFileSync(logFile, updated);
2233
+ }
2234
+
2235
+ function recordAutopilotSuccess(logFile, inboxId, summary) {
2236
+ let content = fs.readFileSync(logFile, 'utf8');
2237
+ if (typeof inboxId === 'number' && !Number.isNaN(inboxId)) {
2238
+ content = removeInboxItemFromContent(content, inboxId);
2239
+ }
2240
+ const nextId = getNextCompletionId(content);
2241
+ content = addCompletionItemToContent(content, nextId, `Autopilot — ${summary}`);
2242
+ fs.writeFileSync(logFile, content);
2243
+ }
2244
+
1606
2245
  function planAtris() {
1607
2246
  const targetDir = path.join(process.cwd(), 'atris');
1608
2247
  const navigatorFile = path.join(targetDir, 'agent_team', 'navigator.md');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "1.4.5",
3
+ "version": "1.5.1",
4
4
  "description": "Universal system instrumentation for AI agents - works for code, product, sales, and more",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {