atris 3.55.0 → 3.56.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/atris/skills/engines/SKILL.md +11 -3
- package/atris/skills/x-search/SKILL.md +20 -1
- package/atris/skills/youtube/SKILL.md +51 -43
- package/bin/atris.js +119 -87
- package/commands/autopilot-front.js +29 -13
- package/commands/brainstorm.js +77 -476
- package/commands/business.js +13 -1
- package/commands/fleet-report.js +2 -2
- package/commands/human-missions.js +26 -2
- package/commands/init.js +2 -35
- package/commands/integrations.js +266 -0
- package/commands/land.js +53 -23
- package/commands/later.js +52 -0
- package/commands/log.js +79 -30
- package/commands/mission.js +117 -22
- package/commands/next.js +153 -63
- package/commands/now.js +80 -38
- package/commands/recap.js +66 -4
- package/commands/run-front.js +20 -5
- package/commands/spaceship.js +52 -12
- package/commands/status.js +30 -7
- package/commands/task.js +70 -16
- package/commands/terminal.js +5 -5
- package/commands/workflow.js +18 -5
- package/commands/x-search.js +123 -13
- package/commands/youtube.js +639 -36
- package/lib/account-bound.js +7 -0
- package/lib/apply-gate.js +94 -0
- package/lib/context-gatherer.js +55 -8
- package/lib/engine-ask.js +16 -6
- package/lib/first-minute.js +552 -26
- package/lib/known-commands.js +1 -1
- package/lib/runner-command.js +5 -3
- package/lib/scratch-root.js +44 -0
- package/package.json +1 -1
- package/utils/auth.js +9 -0
package/commands/log.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
const { getLogPath, ensureLogDirectory, createLogFile, addInboxIdea } = require('../lib/file-ops');
|
|
5
|
+
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
5
6
|
const { isForcedNonInteractive } = require('../lib/noninteractive');
|
|
6
7
|
|
|
7
8
|
function readPipedStdin() {
|
|
@@ -12,42 +13,89 @@ function readPipedStdin() {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function
|
|
16
|
-
|
|
16
|
+
function journalRel(logFile) {
|
|
17
|
+
return (path.relative(process.cwd(), logFile) || logFile).split(path.sep).join('/');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printLogJson(payload) {
|
|
21
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
22
|
+
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
function printReplNeeded(asJson, dateFormatted, rel) {
|
|
25
|
+
const error = `Daily log REPL needs a terminal (${dateFormatted}).`;
|
|
26
|
+
if (asJson) {
|
|
27
|
+
printLogJson({
|
|
28
|
+
ok: false,
|
|
29
|
+
error,
|
|
30
|
+
journal: rel,
|
|
31
|
+
next_command: 'atris log "note"',
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
21
34
|
}
|
|
35
|
+
console.log(error);
|
|
36
|
+
console.log(`journal: ${rel}`);
|
|
37
|
+
console.log('Next: atris log --repl # in a terminal, or atris log "note" / atris wish --json --no-mission');
|
|
38
|
+
}
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
function captureNotes(logFile, notes) {
|
|
41
|
+
return notes.map((note) => addInboxIdea(logFile, note));
|
|
42
|
+
}
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
function printCapture(asJson, ids, notes, rel) {
|
|
45
|
+
if (asJson) {
|
|
46
|
+
const payload = {
|
|
47
|
+
ok: true,
|
|
48
|
+
action: 'inbox_capture',
|
|
49
|
+
journal: rel,
|
|
50
|
+
next_command: 'atris logs',
|
|
51
|
+
};
|
|
52
|
+
if (notes.length === 1) {
|
|
53
|
+
payload.id = `I${ids[0]}`;
|
|
54
|
+
payload.note = notes[0];
|
|
55
|
+
} else {
|
|
56
|
+
payload.count = notes.length;
|
|
57
|
+
payload.ids = ids.map((id) => `I${id}`);
|
|
58
|
+
payload.notes = notes;
|
|
59
|
+
}
|
|
60
|
+
printLogJson(payload);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (notes.length === 1) {
|
|
64
|
+
console.log(`captured I${ids[0]}: ${notes[0]}`);
|
|
65
|
+
console.log(`journal: ${rel}`);
|
|
66
|
+
console.log('Next: atris logs');
|
|
67
|
+
return;
|
|
28
68
|
}
|
|
69
|
+
console.log(`captured ${notes.length} note${notes.length === 1 ? '' : 's'} in ${rel}`);
|
|
70
|
+
}
|
|
29
71
|
|
|
72
|
+
function logAtris() {
|
|
73
|
+
const root = process.cwd();
|
|
30
74
|
const args = process.argv.slice(3);
|
|
31
|
-
const
|
|
75
|
+
const asJson = args.includes('--json');
|
|
32
76
|
const forced = isForcedNonInteractive(args);
|
|
33
77
|
const wantsRepl = args.includes('--repl');
|
|
34
78
|
const positional = args.filter((arg) => !String(arg).startsWith('-'));
|
|
35
79
|
const note = positional.join(' ').trim();
|
|
36
80
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(`journal: ${rel}`);
|
|
42
|
-
console.log('Next: atris logs');
|
|
43
|
-
return;
|
|
81
|
+
// Empty folder talks like bare atris. Do not create atris/ or write
|
|
82
|
+
// a journal. After init, a note still captures.
|
|
83
|
+
if (isFreshWorkspace(root)) {
|
|
84
|
+
process.exit(speakFirstMinute({ root, fresh: true, asJson }));
|
|
44
85
|
}
|
|
45
86
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
ensureLogDirectory();
|
|
88
|
+
const { logFile, dateFormatted } = getLogPath();
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(logFile)) {
|
|
91
|
+
createLogFile(logFile, dateFormatted);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const rel = journalRel(logFile);
|
|
95
|
+
|
|
96
|
+
// `atris log "sentence"` (or one slug-like word) appends to today's Inbox.
|
|
97
|
+
if (note && !wantsRepl) {
|
|
98
|
+
printCapture(asJson, captureNotes(logFile, [note]), [note], rel);
|
|
51
99
|
return;
|
|
52
100
|
}
|
|
53
101
|
|
|
@@ -56,16 +104,17 @@ function logAtris() {
|
|
|
56
104
|
const piped = readPipedStdin();
|
|
57
105
|
const lines = piped.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
58
106
|
const notes = lines.filter((line) => line.toLowerCase() !== 'exit');
|
|
59
|
-
if (notes.length
|
|
60
|
-
|
|
61
|
-
console.log(`journal: ${rel}`);
|
|
62
|
-
console.log('Next: atris log --repl # in a terminal, or atris log "note" / atris logs --json');
|
|
107
|
+
if (notes.length > 0 && !wantsRepl) {
|
|
108
|
+
printCapture(asJson, captureNotes(logFile, notes), notes, rel);
|
|
63
109
|
return;
|
|
64
110
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
111
|
+
printReplNeeded(asJson, dateFormatted, rel);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Forced headless: never open a REPL.
|
|
116
|
+
if (forced) {
|
|
117
|
+
printReplNeeded(asJson, dateFormatted, rel);
|
|
69
118
|
return;
|
|
70
119
|
}
|
|
71
120
|
|
package/commands/mission.js
CHANGED
|
@@ -983,6 +983,62 @@ function hasLocalMissionState(root = process.cwd()) {
|
|
|
983
983
|
.some((mission) => mission && mission.id && mission.cloud !== true);
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
+
const LIVE_MISSION_TICK_MS = 24 * 60 * 60 * 1000;
|
|
987
|
+
|
|
988
|
+
function isLiveInFlightMission(mission, now = Date.now(), root = process.cwd()) {
|
|
989
|
+
if (!mission || !mission.id || mission.cloud === true) return false;
|
|
990
|
+
const status = String(mission.status || '');
|
|
991
|
+
if (TERMINAL_STATUSES.has(status) || status === 'paused' || status === 'blocked') return false;
|
|
992
|
+
if (status === 'running') return true;
|
|
993
|
+
const lastTickAt = Date.parse(mission.last_tick_at || '');
|
|
994
|
+
if (!Number.isFinite(lastTickAt)) return false;
|
|
995
|
+
const age = now - lastTickAt;
|
|
996
|
+
if (!(age >= 0 && age <= LIVE_MISSION_TICK_MS)) return false;
|
|
997
|
+
// ready/planning with a recent receipt is still parked unless a driver is
|
|
998
|
+
// alive. A stalled ready row is the archive, not the live card.
|
|
999
|
+
return missionDriverHealth(mission, root).alive === true;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function pickLiveLocalMission(root = process.cwd()) {
|
|
1003
|
+
return listMissions(root).find((mission) => isLiveInFlightMission(mission, Date.now(), root)) || null;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function wantsBareMissionArchive(args = []) {
|
|
1007
|
+
return hasFlag(args, '--all')
|
|
1008
|
+
&& args.every((value) => value === '--all' || value === '--json');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function missionStatusRef(args = []) {
|
|
1012
|
+
return stripKnownFlags(args, ['--status', '--limit'], ['--json', '--local', '--all'])[0] || '';
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function wantsSpokenMissionStatus(args = []) {
|
|
1016
|
+
if (hasFlag(args, '--all') || hasFlag(args, '--local') || hasFlag(args, '--cloud')) return false;
|
|
1017
|
+
if (readFlag(args, '--status', '') || readFlag(args, '--limit', '')) return false;
|
|
1018
|
+
return !missionStatusRef(args);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function speakMissionDoor(args = [], root = resolveWorkspaceRoot()) {
|
|
1022
|
+
const asJson = args.includes('--json');
|
|
1023
|
+
const live = pickLiveLocalMission(root);
|
|
1024
|
+
if (live) {
|
|
1025
|
+
return statusMission(asJson ? [live.id, '--json'] : [live.id]);
|
|
1026
|
+
}
|
|
1027
|
+
if (isFreshWorkspace(root) || hasLocalMissionState(root)) {
|
|
1028
|
+
const code = speakFirstMinute({
|
|
1029
|
+
root,
|
|
1030
|
+
fresh: isFreshWorkspace(root),
|
|
1031
|
+
asJson,
|
|
1032
|
+
});
|
|
1033
|
+
process.exitCode = code;
|
|
1034
|
+
return code;
|
|
1035
|
+
}
|
|
1036
|
+
return require('./human-missions').currentMissionCommand(args, {
|
|
1037
|
+
root,
|
|
1038
|
+
fallbackOnMissing: true,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
986
1042
|
function terminalNextAction(status) {
|
|
987
1043
|
if (status === 'complete') return 'mission complete';
|
|
988
1044
|
if (status === 'stopped') return 'mission stopped';
|
|
@@ -4811,9 +4867,16 @@ function statusMission(args) {
|
|
|
4811
4867
|
console.log('Use --status active for planning, running, ready, paused, and blocked missions.');
|
|
4812
4868
|
return;
|
|
4813
4869
|
}
|
|
4870
|
+
const root = resolveWorkspaceRoot();
|
|
4871
|
+
if (isFreshWorkspace(root) && !missionStatusRef(args)) {
|
|
4872
|
+
return speakMissionDoor(args, root);
|
|
4873
|
+
}
|
|
4874
|
+
if (wantsSpokenMissionStatus(args)) {
|
|
4875
|
+
return speakMissionDoor(args, root);
|
|
4876
|
+
}
|
|
4814
4877
|
const asJson = wantsJson(args);
|
|
4815
4878
|
const localOnly = hasFlag(args, '--local');
|
|
4816
|
-
const ref =
|
|
4879
|
+
const ref = missionStatusRef(args);
|
|
4817
4880
|
const statusFilter = readFlag(args, '--status', '');
|
|
4818
4881
|
if (statusFilter && !VALID_STATUSES.has(statusFilter) && !STATUS_ALIASES.has(statusFilter)) {
|
|
4819
4882
|
exitMissionError(`Invalid --status: ${statusFilter}`, 2, asJson);
|
|
@@ -4871,7 +4934,7 @@ function statusMission(args) {
|
|
|
4871
4934
|
...missionStatusLandingLines(mission.last_landing),
|
|
4872
4935
|
...(completionGateLabel(mission.completion_gate) ? [` gate: ${completionGateLabel(mission.completion_gate)}`] : []),
|
|
4873
4936
|
])
|
|
4874
|
-
: ['No missions yet.
|
|
4937
|
+
: ['No missions yet.'],
|
|
4875
4938
|
asJson,
|
|
4876
4939
|
);
|
|
4877
4940
|
}
|
|
@@ -9014,7 +9077,7 @@ async function parseAndValidateMissionRunPhase(args) {
|
|
|
9014
9077
|
error: null,
|
|
9015
9078
|
};
|
|
9016
9079
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
9017
|
-
|
|
9080
|
+
console.log('Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]');
|
|
9018
9081
|
context.handled = true;
|
|
9019
9082
|
return context;
|
|
9020
9083
|
}
|
|
@@ -10933,10 +10996,36 @@ async function goalLoopMission(args) {
|
|
|
10933
10996
|
if (completedTaskClosed) process.exitCode = 2;
|
|
10934
10997
|
}
|
|
10935
10998
|
|
|
10936
|
-
function help() {
|
|
10999
|
+
function help(args = []) {
|
|
11000
|
+
if (hasFlag(args, '--full')) {
|
|
11001
|
+
helpFull();
|
|
11002
|
+
return;
|
|
11003
|
+
}
|
|
11004
|
+
console.log(`
|
|
11005
|
+
Usage: atris mission
|
|
11006
|
+
|
|
11007
|
+
Keep working on one goal.
|
|
11008
|
+
|
|
11009
|
+
atris mission what's in front of you
|
|
11010
|
+
atris mission start begin one goal
|
|
11011
|
+
atris mission status same as atris mission
|
|
11012
|
+
atris mission stop stop the live goal
|
|
11013
|
+
atris mission list every saved goal
|
|
11014
|
+
atris mission inspect <id> --fields status,runner,ack,pings
|
|
11015
|
+
|
|
11016
|
+
Hours of keep-working: atris spaceship
|
|
11017
|
+
Keep going until you stop: atris autopilot
|
|
11018
|
+
|
|
11019
|
+
More flags: atris mission help --full
|
|
11020
|
+
`.trim());
|
|
11021
|
+
}
|
|
11022
|
+
|
|
11023
|
+
function helpFull() {
|
|
10937
11024
|
console.log(`
|
|
10938
11025
|
atris mission - durable goal + loop + owner + proof state
|
|
10939
11026
|
|
|
11027
|
+
atris mission Same next as bare atris, or the one live mission
|
|
11028
|
+
atris mission list | --all Full mission archive
|
|
10940
11029
|
atris mission start "<objective>" --owner <member> [--destination "<text>"] [--verify "..."] [--always-on] [--budget quick|long|deep] [--xp-task] [--worktree] [--take-goal-slot]
|
|
10941
11030
|
[--runner manual|claude|atris2|codex_goal] [--model <id>]
|
|
10942
11031
|
(runner claude spawns local claude -p per tick, --model passes through;
|
|
@@ -11372,23 +11461,20 @@ function missionCommand(args) {
|
|
|
11372
11461
|
if (args[0] === 'answer') {
|
|
11373
11462
|
return require('./human-missions').answerCommand(args.slice(1));
|
|
11374
11463
|
}
|
|
11375
|
-
if (isBareMission
|
|
11464
|
+
if (isBareMission) {
|
|
11465
|
+
// Empty folder talks like bare atris. A live in-flight mission shows
|
|
11466
|
+
// that one card. Completed, stopped, stalled, or ready-with-no-driver
|
|
11467
|
+
// rows stay in the archive. After init with no live mission, speak the
|
|
11468
|
+
// desk next.
|
|
11469
|
+
return speakMissionDoor(args);
|
|
11470
|
+
}
|
|
11471
|
+
if (wantsBareMissionArchive(args)) {
|
|
11376
11472
|
const root = resolveWorkspaceRoot();
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
if (isFreshWorkspace(root)) {
|
|
11380
|
-
const code = speakFirstMinute({
|
|
11381
|
-
root,
|
|
11382
|
-
fresh: true,
|
|
11383
|
-
asJson: args.includes('--json'),
|
|
11384
|
-
});
|
|
11385
|
-
process.exitCode = code;
|
|
11386
|
-
return code;
|
|
11387
|
-
}
|
|
11388
|
-
return require('./human-missions').currentMissionCommand(args);
|
|
11473
|
+
if (isFreshWorkspace(root)) return speakMissionDoor(args, root);
|
|
11474
|
+
return statusMission(args);
|
|
11389
11475
|
}
|
|
11390
|
-
const subcommand =
|
|
11391
|
-
const rest =
|
|
11476
|
+
const subcommand = args[0] || 'status';
|
|
11477
|
+
const rest = args.slice(1);
|
|
11392
11478
|
// Every mission verb resolves its state store from process.cwd(). Running one
|
|
11393
11479
|
// from a subdirectory used to create a nested .atris store the fleet never
|
|
11394
11480
|
// reads (proven footgun: a nested .atris appeared under
|
|
@@ -11404,8 +11490,6 @@ function missionCommand(args) {
|
|
|
11404
11490
|
case 'new':
|
|
11405
11491
|
return startMission(rest);
|
|
11406
11492
|
case 'status':
|
|
11407
|
-
case 'list':
|
|
11408
|
-
case 'ls':
|
|
11409
11493
|
case 'show':
|
|
11410
11494
|
case 'info':
|
|
11411
11495
|
case 'view':
|
|
@@ -11416,6 +11500,16 @@ function missionCommand(args) {
|
|
|
11416
11500
|
});
|
|
11417
11501
|
}
|
|
11418
11502
|
return statusMission(rest);
|
|
11503
|
+
case 'list':
|
|
11504
|
+
case 'ls': {
|
|
11505
|
+
if (hasFlag(rest, '--help') || hasFlag(rest, '-h') || String(rest[0] || '').trim() === 'help') {
|
|
11506
|
+
console.log('Usage: atris mission list [--status <state>] [--limit <n>] [--local] [--json]');
|
|
11507
|
+
return;
|
|
11508
|
+
}
|
|
11509
|
+
const listRoot = resolveWorkspaceRoot();
|
|
11510
|
+
if (isFreshWorkspace(listRoot)) return speakMissionDoor(rest, listRoot);
|
|
11511
|
+
return statusMission(rest.includes('--all') ? rest : ['--all', ...rest]);
|
|
11512
|
+
}
|
|
11419
11513
|
case 'doctor':
|
|
11420
11514
|
case 'check':
|
|
11421
11515
|
return doctorMission(rest);
|
|
@@ -11469,7 +11563,7 @@ function missionCommand(args) {
|
|
|
11469
11563
|
case 'help':
|
|
11470
11564
|
case '--help':
|
|
11471
11565
|
case '-h':
|
|
11472
|
-
return help();
|
|
11566
|
+
return help(rest);
|
|
11473
11567
|
default: {
|
|
11474
11568
|
const first = String(subcommand || '');
|
|
11475
11569
|
if (first && !first.startsWith('-')) {
|
|
@@ -11505,6 +11599,7 @@ module.exports = {
|
|
|
11505
11599
|
reapPausedMissions,
|
|
11506
11600
|
missionHeartbeatLines,
|
|
11507
11601
|
listMissions,
|
|
11602
|
+
pickLiveLocalMission,
|
|
11508
11603
|
freezeMissionVerifier,
|
|
11509
11604
|
markMissionReviewReady,
|
|
11510
11605
|
listWorktreeRollupMissions,
|
package/commands/next.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
|
|
4
|
+
const {
|
|
5
|
+
buildFirstMinute,
|
|
6
|
+
folderName,
|
|
7
|
+
freshMinuteJson,
|
|
8
|
+
isFreshWorkspace,
|
|
9
|
+
listUserVisibleWork,
|
|
10
|
+
} = require('../lib/first-minute');
|
|
11
|
+
const { loadContext } = require('../lib/state-detection');
|
|
3
12
|
const {
|
|
4
13
|
claimRoadmapItem,
|
|
5
14
|
nextCards,
|
|
@@ -9,70 +18,146 @@ const {
|
|
|
9
18
|
seedInboxFromMove,
|
|
10
19
|
} = require('../lib/next-moves');
|
|
11
20
|
|
|
12
|
-
function showHelp() {
|
|
13
|
-
|
|
14
|
-
console.log('Usage: atris next [yes|no|skip]');
|
|
15
|
-
console.log('');
|
|
16
|
-
console.log('Shows one next move card.');
|
|
17
|
-
console.log('');
|
|
21
|
+
function showHelp(log = console.log) {
|
|
22
|
+
log('Usage: atris next [--json]');
|
|
18
23
|
return 0;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
function
|
|
22
|
-
return String(
|
|
26
|
+
function spokenWin(text) {
|
|
27
|
+
return String(text || '')
|
|
28
|
+
.split('\n')
|
|
29
|
+
.map((line) => line.trim())
|
|
30
|
+
.find(Boolean) || '';
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
function minuteText(screen) {
|
|
34
|
+
const win = spokenWin(screen && screen.text);
|
|
35
|
+
const next = String(screen && screen.nextCommand || '').trim();
|
|
36
|
+
if (!next) return win || 'nothing is waiting.';
|
|
37
|
+
return `${win}\n\nnext: ${next}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function minuteJson(screen, extra = {}) {
|
|
41
|
+
const reason = spokenWin(screen && screen.text).replace(/\.$/, '');
|
|
42
|
+
const next = String((screen && screen.nextCommand) || extra.next_action || '').trim();
|
|
43
|
+
const fresh = extra.fresh === true;
|
|
44
|
+
return {
|
|
45
|
+
schema: 'atris.one_lap.v1',
|
|
46
|
+
ok: Boolean(next) && !fresh,
|
|
47
|
+
status: fresh ? 'stuck' : (next ? 'ok' : 'stuck'),
|
|
48
|
+
reason,
|
|
49
|
+
next_action: next,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printMinute(screen, { asJson = false, log = console.log, fresh = false } = {}) {
|
|
54
|
+
if (asJson) {
|
|
55
|
+
log(JSON.stringify(minuteJson(screen, { fresh }), null, 2));
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
log('');
|
|
59
|
+
log(minuteText(screen));
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function nextScreen(root) {
|
|
64
|
+
const fresh = isFreshWorkspace(root);
|
|
65
|
+
if (fresh) {
|
|
66
|
+
return {
|
|
67
|
+
fresh: true,
|
|
68
|
+
screen: buildFirstMinute({ root, fresh: true }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
fresh: false,
|
|
73
|
+
screen: buildFirstMinute({
|
|
74
|
+
root,
|
|
75
|
+
fresh: false,
|
|
76
|
+
context: loadContext(root),
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function speakNext(root, { asJson = false, log = console.log } = {}) {
|
|
82
|
+
const { fresh, screen } = nextScreen(root);
|
|
83
|
+
if (asJson && fresh) {
|
|
84
|
+
log(JSON.stringify(freshMinuteJson(folderName(root), listUserVisibleWork(root), { root }), null, 2));
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
return printMinute(screen, { asJson, log, fresh });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cardLabel(card) {
|
|
91
|
+
return String(card?.label || card?.title || '').trim();
|
|
32
92
|
}
|
|
33
93
|
|
|
34
|
-
function
|
|
35
|
-
if (!card) return '
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
94
|
+
function cardNextCommand(card) {
|
|
95
|
+
if (!card) return '';
|
|
96
|
+
const action = card.next_action || {};
|
|
97
|
+
const prompt = String(action.prompt || '').trim();
|
|
98
|
+
if (/^(atris|ax)\b/i.test(prompt)) return prompt;
|
|
99
|
+
if (action.type === 'mission_complete' && action.mission_id) {
|
|
100
|
+
if (action.proof_path) {
|
|
101
|
+
return `atris mission complete ${action.mission_id} --proof ${action.proof_path}`;
|
|
102
|
+
}
|
|
103
|
+
return `atris mission status ${action.mission_id}`;
|
|
104
|
+
}
|
|
105
|
+
if (action.type === 'mission_review_prompt' && action.mission_id) {
|
|
106
|
+
return `atris mission status ${action.mission_id}`;
|
|
107
|
+
}
|
|
108
|
+
if (action.type === 'wish_answer_prompt') return 'atris wish answer "your words"';
|
|
109
|
+
if (card.source === 'inbox') return 'atris plan';
|
|
110
|
+
if (card.source === 'task' || card.source === 'roadmap' || card.source === 'endgame') {
|
|
111
|
+
return 'atris do';
|
|
112
|
+
}
|
|
113
|
+
if (card.source === 'mission') {
|
|
114
|
+
const id = action.mission_id || card.ref;
|
|
115
|
+
return id ? `atris mission status ${id}` : 'atris mission status --status active';
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
41
118
|
}
|
|
42
119
|
|
|
43
|
-
function
|
|
44
|
-
|
|
120
|
+
function speakCard(card, { asJson = false, log = console.log } = {}) {
|
|
121
|
+
if (!card) {
|
|
122
|
+
return printMinute({ text: 'nothing is waiting.', nextCommand: '' }, { asJson, log });
|
|
123
|
+
}
|
|
124
|
+
const next = cardNextCommand(card);
|
|
125
|
+
const label = cardLabel(card) || 'this';
|
|
126
|
+
return printMinute({
|
|
127
|
+
text: `${label} is waiting.`,
|
|
128
|
+
nextCommand: next,
|
|
129
|
+
}, { asJson, log });
|
|
45
130
|
}
|
|
46
131
|
|
|
47
132
|
function wishPromptTitle(label) {
|
|
48
133
|
return String(label || 'this').replace(/^(#\d+)\s+/, '$1: ');
|
|
49
134
|
}
|
|
50
135
|
|
|
51
|
-
function printWishAnswerPrompt(card) {
|
|
136
|
+
function printWishAnswerPrompt(card, log = console.log) {
|
|
52
137
|
const action = card.next_action || {};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
138
|
+
log(`Got it, wish ${wishPromptTitle(action.label || cardLabel(card))}.`);
|
|
139
|
+
log(String(action.question || 'What should be different when this wish comes true?'));
|
|
140
|
+
log('Answer with: atris wish answer "your words"');
|
|
56
141
|
return 0;
|
|
57
142
|
}
|
|
58
143
|
|
|
59
|
-
function approveMove(card, root) {
|
|
144
|
+
function approveMove(card, root, log = console.log) {
|
|
60
145
|
recordDecision(root, card, 'approve', new Date().toISOString());
|
|
61
146
|
if (['roadmap', 'inbox', 'endgame'].includes(card.source)) {
|
|
62
147
|
seedInboxFromMove(root, card);
|
|
63
148
|
if (card.source === 'roadmap') claimRoadmapItem(root, card.title);
|
|
64
|
-
|
|
149
|
+
log(`${cardLabel(card)} is working.`);
|
|
65
150
|
return 0;
|
|
66
151
|
}
|
|
67
152
|
const prompt = card.next_action && card.next_action.prompt;
|
|
68
|
-
|
|
153
|
+
log(prompt || `${cardLabel(card)} is working.`);
|
|
69
154
|
return 0;
|
|
70
155
|
}
|
|
71
156
|
|
|
72
|
-
function completeMissionFromCard(card) {
|
|
157
|
+
function completeMissionFromCard(card, log = console.log) {
|
|
73
158
|
const action = card.next_action || {};
|
|
74
159
|
if (!action.mission_id || !action.proof_path) {
|
|
75
|
-
|
|
160
|
+
log('Review the proof, then complete this mission.');
|
|
76
161
|
return 0;
|
|
77
162
|
}
|
|
78
163
|
const { completeMission } = require('./mission');
|
|
@@ -80,57 +165,62 @@ function completeMissionFromCard(card) {
|
|
|
80
165
|
return 0;
|
|
81
166
|
}
|
|
82
167
|
|
|
83
|
-
function executeCard(card, root) {
|
|
168
|
+
function executeCard(card, root, log = console.log) {
|
|
84
169
|
const action = card && card.next_action;
|
|
85
|
-
if (!card) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
if (action?.type === 'wish_answer_prompt') return printWishAnswerPrompt(card);
|
|
90
|
-
if (action?.type === 'mission_complete') return completeMissionFromCard(card);
|
|
170
|
+
if (!card) return speakCard(null, { log });
|
|
171
|
+
if (action?.type === 'wish_answer_prompt') return printWishAnswerPrompt(card, log);
|
|
172
|
+
if (action?.type === 'mission_complete') return completeMissionFromCard(card, log);
|
|
91
173
|
if (action?.type === 'wish_review_prompt' || action?.type === 'mission_review_prompt') {
|
|
92
|
-
|
|
174
|
+
log(action.prompt || 'Review the proof, then choose done or stuck.');
|
|
93
175
|
return 0;
|
|
94
176
|
}
|
|
95
|
-
return approveMove(card, root);
|
|
177
|
+
return approveMove(card, root, log);
|
|
96
178
|
}
|
|
97
179
|
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
|
|
180
|
+
function actionToken(args = []) {
|
|
181
|
+
const list = Array.isArray(args) ? args : [];
|
|
182
|
+
const token = list.find((arg) => {
|
|
183
|
+
const text = String(arg || '').trim();
|
|
184
|
+
if (!text || text.startsWith('-')) return false;
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
return String(token || '').trim().toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function nextCommand(args = [], root = process.cwd(), { log = console.log } = {}) {
|
|
191
|
+
const list = Array.isArray(args) ? args : [];
|
|
192
|
+
if (argsWantHelp(list)) return showHelp(log);
|
|
193
|
+
|
|
194
|
+
const asJson = wantsJson(list);
|
|
195
|
+
const action = actionToken(list);
|
|
196
|
+
|
|
197
|
+
if (!action || action === 'json') return speakNext(root, { asJson, log });
|
|
101
198
|
|
|
102
199
|
const current = nextCards(root, 1)[0] || null;
|
|
103
|
-
if (!action) {
|
|
104
|
-
printCard(current);
|
|
105
|
-
if (current?.source === 'dream') markDreamCardConsumed(root, current, new Date().toISOString(), 'dealt');
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
200
|
|
|
109
201
|
if (action === 'skip') {
|
|
110
202
|
const following = current ? nextCards(root, 1, { skipIds: [current.id] })[0] : null;
|
|
111
|
-
if (current?.source === 'dream')
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
if (current?.source === 'dream') {
|
|
204
|
+
markDreamCardConsumed(root, current, new Date().toISOString(), 'skipped');
|
|
205
|
+
}
|
|
206
|
+
if (following?.source === 'dream') {
|
|
207
|
+
markDreamCardConsumed(root, following, new Date().toISOString(), 'dealt');
|
|
208
|
+
}
|
|
209
|
+
return speakCard(following, { asJson, log });
|
|
115
210
|
}
|
|
116
211
|
|
|
117
212
|
if (action === 'no') {
|
|
118
|
-
if (!current) {
|
|
119
|
-
printCard(null);
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
213
|
+
if (!current) return speakCard(null, { asJson, log });
|
|
122
214
|
parkNextCard(root, current);
|
|
123
|
-
|
|
215
|
+
log(`Parked ${cardLabel(current)}.`);
|
|
124
216
|
return 0;
|
|
125
217
|
}
|
|
126
218
|
|
|
127
|
-
if (action === 'yes') return executeCard(current, root);
|
|
219
|
+
if (action === 'yes') return executeCard(current, root, log);
|
|
128
220
|
|
|
129
|
-
|
|
130
|
-
return 2;
|
|
221
|
+
return speakNext(root, { asJson, log });
|
|
131
222
|
}
|
|
132
223
|
|
|
133
224
|
module.exports = {
|
|
134
225
|
nextCommand,
|
|
135
|
-
renderCard,
|
|
136
226
|
};
|