atris 3.56.0 → 3.56.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/atris/skills/engines/SKILL.md +11 -3
  2. package/atris/skills/x-search/SKILL.md +20 -1
  3. package/atris/skills/youtube/SKILL.md +51 -43
  4. package/bin/atris.js +134 -90
  5. package/commands/auth.js +6 -1
  6. package/commands/autopilot-front.js +29 -13
  7. package/commands/brainstorm.js +77 -476
  8. package/commands/business.js +13 -1
  9. package/commands/engine.js +7 -0
  10. package/commands/experiments.js +178 -0
  11. package/commands/fleet-report.js +2 -2
  12. package/commands/founder.js +12 -0
  13. package/commands/human-missions.js +64 -2
  14. package/commands/init.js +2 -35
  15. package/commands/integrations.js +100 -0
  16. package/commands/land.js +53 -23
  17. package/commands/later.js +52 -0
  18. package/commands/launchpad.js +45 -10
  19. package/commands/log.js +79 -30
  20. package/commands/mission.js +117 -22
  21. package/commands/next.js +153 -63
  22. package/commands/now.js +115 -38
  23. package/commands/recap.js +97 -4
  24. package/commands/run-front.js +20 -5
  25. package/commands/spaceship.js +52 -12
  26. package/commands/status.js +38 -7
  27. package/commands/task.js +56 -19
  28. package/commands/terminal.js +5 -5
  29. package/commands/workflow.js +19 -5
  30. package/commands/x-search.js +123 -13
  31. package/commands/youtube.js +941 -36
  32. package/lib/account-bound.js +7 -0
  33. package/lib/apply-gate.js +102 -0
  34. package/lib/config-guard.js +106 -0
  35. package/lib/context-gatherer.js +55 -8
  36. package/lib/engine-ask.js +16 -6
  37. package/lib/engine-registry.js +14 -4
  38. package/lib/first-minute.js +571 -26
  39. package/lib/known-commands.js +1 -1
  40. package/lib/pack-capabilities.js +13 -3
  41. package/lib/runner-command.js +5 -3
  42. package/lib/scratch-root.js +44 -0
  43. package/lib/workspace-scaffold.js +3 -1
  44. package/package.json +1 -1
  45. package/utils/auth.js +84 -6
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
- console.log('');
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 cardLabel(card) {
22
- return String(card?.label || card?.title || '').trim();
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 cardWhy(card) {
26
- const why = String(card?.why || '').trim();
27
- if (why) return why;
28
- const status = String(card?.status || '').trim();
29
- if (status) return status;
30
- if (card?.source === 'task' || card?.source === 'mission') return 'working';
31
- return 'new';
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 renderCard(card) {
35
- if (!card) return 'Nothing to do. Rest or wish.';
36
- return [
37
- cardLabel(card),
38
- `Why now: ${cardWhy(card)}`,
39
- 'Do it? yes / no / skip',
40
- ].join('\n');
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 printCard(card) {
44
- console.log(renderCard(card));
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
- console.log(`Got it, wish ${wishPromptTitle(action.label || cardLabel(card))}.`);
54
- console.log(String(action.question || 'What should be different when this wish comes true?'));
55
- console.log('Answer with: atris wish answer "your words"');
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
- console.log(`${cardLabel(card)} is working.`);
149
+ log(`${cardLabel(card)} is working.`);
65
150
  return 0;
66
151
  }
67
152
  const prompt = card.next_action && card.next_action.prompt;
68
- console.log(prompt || `${cardLabel(card)} is working.`);
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
- console.log('Review the proof, then complete this mission.');
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
- printCard(null);
87
- return 0;
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
- console.log(action.prompt || 'Review the proof, then choose done or stuck.');
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 nextCommand(args = [], root = process.cwd()) {
99
- const action = String(args[0] || '').trim().toLowerCase();
100
- if (action === 'help' || action === '--help' || action === '-h') return showHelp();
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') markDreamCardConsumed(root, current, new Date().toISOString(), 'skipped');
112
- printCard(following);
113
- if (following?.source === 'dream') markDreamCardConsumed(root, following, new Date().toISOString(), 'dealt');
114
- return 0;
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
- console.log(`Parked ${cardLabel(current)}.`);
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
- console.log('Say yes, no, or skip.');
130
- return 2;
221
+ return speakNext(root, { asJson, log });
131
222
  }
132
223
 
133
224
  module.exports = {
134
225
  nextCommand,
135
- renderCard,
136
226
  };
package/commands/now.js CHANGED
@@ -3,6 +3,13 @@ const path = require('path');
3
3
  const { hasRenderedSections, isOpenSection } = require('../lib/todo-sections');
4
4
  const { renderMorningCardRow } = require('../lib/receipt-block');
5
5
  const { historicalLandingText } = require('../lib/autoland');
6
+ const {
7
+ buildFirstMinute,
8
+ isFreshWorkspace,
9
+ isKeepWorkingMinute,
10
+ speakFirstMinute,
11
+ speakKeepWorkingMinute,
12
+ } = require('../lib/first-minute');
6
13
 
7
14
  const NOW_PATH = path.join('atris', 'now.md');
8
15
  const TASK_EPISODES_PATH = path.join('.atris', 'state', 'task_episodes.jsonl');
@@ -576,6 +583,39 @@ function refreshNowFile(root = process.cwd(), options = {}) {
576
583
  return { path: nowPath, preserved: false };
577
584
  }
578
585
 
586
+ function stripAnsi(text) {
587
+ return String(text || '').replace(/\u001b\[[0-9;]*m/g, '');
588
+ }
589
+
590
+ function spokenCurrent(text) {
591
+ const line = stripAnsi(text).split(/\n/).map((part) => part.trim()).find(Boolean) || '';
592
+ return line.replace(/^hey\s+[^,]+,\s*/i, '').replace(/\.$/, '');
593
+ }
594
+
595
+ function printNowJson(payload) {
596
+ console.log(JSON.stringify(payload, null, 2));
597
+ }
598
+
599
+ function hasLiveKeepWorkingRun(root = process.cwd()) {
600
+ try {
601
+ const { pickLiveLocalMission } = require('./mission');
602
+ return Boolean(pickLiveLocalMission(root));
603
+ } catch {
604
+ return false;
605
+ }
606
+ }
607
+
608
+ function nowJsonPayload(root = process.cwd()) {
609
+ const fresh = isFreshWorkspace(root);
610
+ const screen = buildFirstMinute({ root, fresh });
611
+ const payload = { ok: !fresh };
612
+ const current = spokenCurrent(screen && screen.text);
613
+ const next = stripAnsi(screen && screen.nextCommand);
614
+ if (current) payload.current = current;
615
+ if (next) payload.next = next;
616
+ return payload;
617
+ }
618
+
579
619
  function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
580
620
  const help = args.includes('--help') || args.includes('-h') || args[0] === 'help';
581
621
  if (help) {
@@ -588,8 +628,8 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
588
628
  console.log(' atris now --refresh Regenerate a small local now.md');
589
629
  console.log(' atris now --all Refresh this parent and every child Atris workspace');
590
630
  console.log(' atris now --path Print the file path only');
591
- console.log(' atris now --json Emit path and content as JSON');
592
- return;
631
+ console.log(' atris now --json Print ok, next or current as JSON');
632
+ return 0;
593
633
  }
594
634
 
595
635
  const init = args.includes('--init');
@@ -598,48 +638,85 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
598
638
  const pathOnly = args.includes('--path');
599
639
  const asJson = args.includes('--json');
600
640
 
601
- let result;
602
- if (all) {
603
- const workspaces = findChildWorkspaces(root);
604
- for (const workspace of workspaces) {
605
- refreshNowFile(workspace.root);
641
+ try {
642
+ if (asJson && !init && !refresh && !all && !pathOnly) {
643
+ const payload = nowJsonPayload(root);
644
+ printNowJson(payload);
645
+ return payload.ok ? 0 : 2;
606
646
  }
607
- result = refreshNowFile(root);
608
- if (!pathOnly && !asJson) {
609
- console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
610
- console.log('');
647
+
648
+ // Fresh folder: empty talks first-talk. A file already here
649
+ // names that file, same as bare atris / now --json. Do not mint.
650
+ // Just-minted file folder, nothing running: same two lines as
651
+ // first-minute / status / recap. Not factory MAP.md.
652
+ // After init, next is claim: same two lines as bare atris /
653
+ // now --json. Not factory now.md.
654
+ // --init / --refresh / --all / --path still write now.md.
655
+ if (!init && !refresh && !all && !pathOnly) {
656
+ if (isFreshWorkspace(root)) {
657
+ return speakFirstMinute({ root, fresh: true });
658
+ }
659
+ if (!hasLiveKeepWorkingRun(root)) {
660
+ const minute = buildFirstMinute({ root });
661
+ if (isKeepWorkingMinute(minute)) {
662
+ return speakKeepWorkingMinute({ root });
663
+ }
664
+ return speakFirstMinute({ root });
665
+ }
611
666
  }
612
- } else if (refresh) {
613
- result = refreshNowFile(root);
614
- } else if (init) {
615
- result = ensureNowFile(root);
616
- } else {
617
- result = ensureNowFile(root);
618
- }
619
667
 
620
- const rel = path.relative(root, result.path);
621
- if (pathOnly) {
622
- console.log(rel);
623
- return;
624
- }
668
+ let result;
669
+ if (all) {
670
+ const workspaces = findChildWorkspaces(root);
671
+ for (const workspace of workspaces) {
672
+ refreshNowFile(workspace.root);
673
+ }
674
+ result = refreshNowFile(root);
675
+ if (!pathOnly && !asJson) {
676
+ console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
677
+ console.log('');
678
+ }
679
+ } else if (refresh) {
680
+ result = refreshNowFile(root);
681
+ } else if (init) {
682
+ result = ensureNowFile(root);
683
+ } else {
684
+ result = ensureNowFile(root);
685
+ }
625
686
 
626
- const content = fs.readFileSync(result.path, 'utf8').trimEnd();
627
- if (asJson) {
628
- console.log(JSON.stringify({
629
- ok: true,
630
- path: rel,
631
- created: Boolean(result.created),
632
- content,
633
- }, null, 2));
634
- return;
635
- }
687
+ const rel = path.relative(root, result.path);
688
+ if (pathOnly) {
689
+ console.log(rel);
690
+ return 0;
691
+ }
636
692
 
637
- if (result.created) {
638
- console.log(`Created ${rel}`);
639
- console.log('');
640
- }
693
+ if (asJson) {
694
+ const payload = nowJsonPayload(root);
695
+ printNowJson(payload);
696
+ return payload.ok ? 0 : 2;
697
+ }
698
+
699
+ const content = fs.readFileSync(result.path, 'utf8').trimEnd();
700
+ if (result.created) {
701
+ console.log(`Created ${rel}`);
702
+ console.log('');
703
+ }
641
704
 
642
- console.log(content);
705
+ console.log(content);
706
+ return 0;
707
+ } catch (err) {
708
+ const message = stripAnsi(err && err.message ? err.message : String(err));
709
+ if (asJson) {
710
+ printNowJson({
711
+ ok: false,
712
+ current: message.replace(/\.$/, ''),
713
+ next: 'atris init --yes',
714
+ });
715
+ return 2;
716
+ }
717
+ console.error(message);
718
+ return 1;
719
+ }
643
720
  }
644
721
 
645
722
  module.exports = {
package/commands/recap.js CHANGED
@@ -1,6 +1,17 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { isCertifiedReview, personName } = require('../lib/first-minute');
3
+ const {
4
+ isCertifiedReview,
5
+ isFreshWorkspace,
6
+ isKeepWorkingMinute,
7
+ isClaimMinute,
8
+ buildFirstMinute,
9
+ listUserVisibleWork,
10
+ personName,
11
+ speakFirstMinute,
12
+ speakKeepWorkingMinute,
13
+ taskCommand,
14
+ } = require('../lib/first-minute');
4
15
  const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
5
16
 
6
17
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -241,6 +252,20 @@ function recapSoftTitle(title, maxWords = 5) {
241
252
  return `"${text.toLowerCase()}"`;
242
253
  }
243
254
 
255
+ function hasLiveKeepWorkingRun(root = process.cwd()) {
256
+ try {
257
+ const { pickLiveLocalMission } = require('./mission');
258
+ return Boolean(pickLiveLocalMission(root));
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ function pickSpokenInProgress(items) {
265
+ const list = Array.isArray(items) ? items.filter(Boolean) : [];
266
+ return list.find((item) => item.owner) || list[0] || null;
267
+ }
268
+
244
269
  function renderRecapMinute(data, { person } = {}) {
245
270
  const who = person != null ? person : personName();
246
271
  const greet = who ? `hey ${who}, ` : '';
@@ -290,9 +315,15 @@ function renderRecapMinute(data, { person } = {}) {
290
315
  }
291
316
 
292
317
  if (inProgress.length) {
293
- const item = inProgress[0];
318
+ const item = pickSpokenInProgress(inProgress);
294
319
  const named = recapSoftTitle(item && item.title);
295
- if (item && item.owner && named) return `${greet}${named} is already yours.`;
320
+ if (item && item.owner && named) {
321
+ return [
322
+ `${greet}${named} is already yours.`,
323
+ '',
324
+ `next: ${taskCommand({ display_id: item.id, status: 'claimed' })}`,
325
+ ].join('\n');
326
+ }
296
327
  if (named) return `${greet}${named} is ready to claim.`;
297
328
  }
298
329
 
@@ -319,9 +350,71 @@ function recapAtris(args = []) {
319
350
  printRecapHelp();
320
351
  return;
321
352
  }
353
+ const cwd = process.cwd();
354
+ const taskDb = loadTaskDb();
355
+ let root = cwd;
356
+ try {
357
+ root = taskDb ? taskDb.workspaceRoot(cwd) : cwd;
358
+ } catch {
359
+ root = cwd;
360
+ }
361
+ // cd src in a real git project still recaps that project. An empty
362
+ // child under /tmp stays its own room and does not inherit /tmp.
363
+ let inProjectSubdir = false;
364
+ try {
365
+ inProjectSubdir = fs.realpathSync(cwd) !== fs.realpathSync(root);
366
+ } catch {
367
+ inProjectSubdir = path.resolve(cwd) !== path.resolve(root);
368
+ }
369
+ const visibleBefore = listUserVisibleWork(root);
322
370
  const daysIdx = args.indexOf('--days');
323
371
  const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
324
- const data = buildRecapData(process.cwd(), { days });
372
+ const data = buildRecapData(root, { days });
373
+ if (
374
+ data.empty
375
+ && isFreshWorkspace(root)
376
+ && !inProjectSubdir
377
+ && !args.includes('--verbose')
378
+ && !args.includes('--full')
379
+ && !args.includes('--share')
380
+ ) {
381
+ return speakFirstMinute({
382
+ root,
383
+ asJson: args.includes('--json'),
384
+ files: visibleBefore,
385
+ });
386
+ }
387
+ // Just-minted file folder, nothing running: same two lines as
388
+ // first-minute / status / recap. After the work is yours, next
389
+ // is task ready so keep-working is not a do loop. Not factory MAP.md.
390
+ // After init, next is claim: same two lines as bare atris /
391
+ // status / now / stop. --json keeps the recap receipt and
392
+ // fills next from first-minute so it is not silent.
393
+ // A claimed non-seed task still recaps that work. A live mission
394
+ // and --verbose / --share keep the recap report.
395
+ if (
396
+ !args.includes('--verbose')
397
+ && !args.includes('--full')
398
+ && !args.includes('--share')
399
+ && !inProjectSubdir
400
+ && !hasLiveKeepWorkingRun(root)
401
+ ) {
402
+ const minute = buildFirstMinute({ root, files: visibleBefore });
403
+ if (isKeepWorkingMinute(minute)) {
404
+ return speakKeepWorkingMinute({
405
+ root,
406
+ asJson: args.includes('--json'),
407
+ });
408
+ }
409
+ if (isClaimMinute(minute)) {
410
+ if (args.includes('--json')) {
411
+ data.next = minute.nextCommand || null;
412
+ console.log(JSON.stringify(data, null, 2));
413
+ return;
414
+ }
415
+ return speakFirstMinute({ root });
416
+ }
417
+ }
325
418
  if (args.includes('--json')) {
326
419
  console.log(JSON.stringify(data, null, 2));
327
420
  return;
@@ -1,9 +1,12 @@
1
1
  // atris run: thin front door over the mission runtime loop.
2
2
  // One bounded pursuit: start a mission from the objective (or resume the most
3
- // logical runnable mission), tick it, complete on pass, then exit.
3
+ // logical runnable mission after --yes), then exit.
4
+ // A hours / keep-working mission is never started from a bare invoke.
4
5
  // The old plan→do→review loop lives on behind `atris run --legacy`.
5
6
  const path = require('path');
6
7
  const { spawnSync } = require('child_process');
8
+ const { hasYesFlag, wantsJson } = require('../lib/noninteractive');
9
+ const { speakFirstMinute } = require('../lib/first-minute');
7
10
 
8
11
  const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
9
12
 
@@ -91,6 +94,14 @@ function pickRunnableMission(root = process.cwd(), missionMap = null, options =
91
94
  return candidates[0] || null;
92
95
  }
93
96
 
97
+ function isHoursMission(mission) {
98
+ if (!mission) return false;
99
+ if (mission.overnight_loop) return true;
100
+ const text = `${mission.objective || ''} ${mission.stop_condition || ''}`;
101
+ if (/\b(overnight|nonstop|forever|goal\s+after\s+goal|self[-\s]?improve)\b/i.test(text)) return true;
102
+ return /\bhours?\b/i.test(text);
103
+ }
104
+
94
105
  function runTickBudget(args, budgetSeconds) {
95
106
  const explicit = positiveNumber(readValueFlag(args, '--max-ticks'));
96
107
  if (explicit) return Math.floor(explicit);
@@ -122,14 +133,17 @@ async function runMissionFront(args = []) {
122
133
  return result.ok ? 0 : 1;
123
134
  }
124
135
 
136
+ // Bare `atris run` talks like the desk. A long keep-working mission
137
+ // needs an objective or an explicit --yes. Headless never prompts.
138
+ if (!hasYesFlag(args)) {
139
+ return speakFirstMinute({ asJson: wantsJson(args) });
140
+ }
141
+
125
142
  const mission = pickRunnableMission(process.cwd(), null, {
126
143
  allowCallerSessionRunners: liveCodexSession(),
127
144
  });
128
145
  if (!mission) {
129
- console.log('No objective given and no runnable mission found.');
130
- console.log('Start one: atris run "<objective>" [--minutes N] [--owner <member>]');
131
- console.log('Or keep going indefinitely: atris autopilot');
132
- return 1;
146
+ return speakFirstMinute({ asJson: wantsJson(args) });
133
147
  }
134
148
  console.log(`Resuming mission ${mission.id} (${mission.status}): ${mission.objective}`);
135
149
  const result = spawnSync(process.execPath, [
@@ -144,6 +158,7 @@ async function runMissionFront(args = []) {
144
158
  module.exports = {
145
159
  runMissionFront,
146
160
  pickRunnableMission,
161
+ isHoursMission,
147
162
  runObjective,
148
163
  runBudgetSeconds,
149
164
  runTickBudget,