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.
- package/GETTING_STARTED.md +10 -0
- package/README.md +10 -0
- package/atris/GETTING_STARTED.md +10 -0
- package/bin/atris.js +698 -59
- package/package.json +1 -1
package/GETTING_STARTED.md
CHANGED
|
@@ -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)
|
package/atris/GETTING_STARTED.md
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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');
|