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/commands/now.js CHANGED
@@ -576,6 +576,31 @@ function refreshNowFile(root = process.cwd(), options = {}) {
576
576
  return { path: nowPath, preserved: false };
577
577
  }
578
578
 
579
+ function stripAnsi(text) {
580
+ return String(text || '').replace(/\u001b\[[0-9;]*m/g, '');
581
+ }
582
+
583
+ function spokenCurrent(text) {
584
+ const line = stripAnsi(text).split(/\n/).map((part) => part.trim()).find(Boolean) || '';
585
+ return line.replace(/^hey\s+[^,]+,\s*/i, '').replace(/\.$/, '');
586
+ }
587
+
588
+ function printNowJson(payload) {
589
+ console.log(JSON.stringify(payload, null, 2));
590
+ }
591
+
592
+ function nowJsonPayload(root = process.cwd()) {
593
+ const { buildFirstMinute, isFreshWorkspace } = require('../lib/first-minute');
594
+ const fresh = isFreshWorkspace(root);
595
+ const screen = buildFirstMinute({ root, fresh });
596
+ const payload = { ok: !fresh };
597
+ const current = spokenCurrent(screen && screen.text);
598
+ const next = stripAnsi(screen && screen.nextCommand);
599
+ if (current) payload.current = current;
600
+ if (next) payload.next = next;
601
+ return payload;
602
+ }
603
+
579
604
  function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
580
605
  const help = args.includes('--help') || args.includes('-h') || args[0] === 'help';
581
606
  if (help) {
@@ -588,8 +613,8 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
588
613
  console.log(' atris now --refresh Regenerate a small local now.md');
589
614
  console.log(' atris now --all Refresh this parent and every child Atris workspace');
590
615
  console.log(' atris now --path Print the file path only');
591
- console.log(' atris now --json Emit path and content as JSON');
592
- return;
616
+ console.log(' atris now --json Print ok, next or current as JSON');
617
+ return 0;
593
618
  }
594
619
 
595
620
  const init = args.includes('--init');
@@ -598,48 +623,65 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
598
623
  const pathOnly = args.includes('--path');
599
624
  const asJson = args.includes('--json');
600
625
 
601
- let result;
602
- if (all) {
603
- const workspaces = findChildWorkspaces(root);
604
- for (const workspace of workspaces) {
605
- refreshNowFile(workspace.root);
626
+ try {
627
+ if (asJson && !init && !refresh && !all && !pathOnly) {
628
+ const payload = nowJsonPayload(root);
629
+ printNowJson(payload);
630
+ return payload.ok ? 0 : 2;
606
631
  }
607
- result = refreshNowFile(root);
608
- if (!pathOnly && !asJson) {
609
- console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
610
- console.log('');
632
+
633
+ let result;
634
+ if (all) {
635
+ const workspaces = findChildWorkspaces(root);
636
+ for (const workspace of workspaces) {
637
+ refreshNowFile(workspace.root);
638
+ }
639
+ result = refreshNowFile(root);
640
+ if (!pathOnly && !asJson) {
641
+ console.log(`Refreshed ${workspaces.length} child workspace${workspaces.length === 1 ? '' : 's'}.`);
642
+ console.log('');
643
+ }
644
+ } else if (refresh) {
645
+ result = refreshNowFile(root);
646
+ } else if (init) {
647
+ result = ensureNowFile(root);
648
+ } else {
649
+ result = ensureNowFile(root);
611
650
  }
612
- } else if (refresh) {
613
- result = refreshNowFile(root);
614
- } else if (init) {
615
- result = ensureNowFile(root);
616
- } else {
617
- result = ensureNowFile(root);
618
- }
619
651
 
620
- const rel = path.relative(root, result.path);
621
- if (pathOnly) {
622
- console.log(rel);
623
- return;
624
- }
652
+ const rel = path.relative(root, result.path);
653
+ if (pathOnly) {
654
+ console.log(rel);
655
+ return 0;
656
+ }
625
657
 
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
- }
658
+ if (asJson) {
659
+ const payload = nowJsonPayload(root);
660
+ printNowJson(payload);
661
+ return payload.ok ? 0 : 2;
662
+ }
636
663
 
637
- if (result.created) {
638
- console.log(`Created ${rel}`);
639
- console.log('');
640
- }
664
+ const content = fs.readFileSync(result.path, 'utf8').trimEnd();
665
+ if (result.created) {
666
+ console.log(`Created ${rel}`);
667
+ console.log('');
668
+ }
641
669
 
642
- console.log(content);
670
+ console.log(content);
671
+ return 0;
672
+ } catch (err) {
673
+ const message = stripAnsi(err && err.message ? err.message : String(err));
674
+ if (asJson) {
675
+ printNowJson({
676
+ ok: false,
677
+ current: message.replace(/\.$/, ''),
678
+ next: 'atris init --yes',
679
+ });
680
+ return 2;
681
+ }
682
+ console.error(message);
683
+ return 1;
684
+ }
643
685
  }
644
686
 
645
687
  module.exports = {
package/commands/recap.js CHANGED
@@ -1,6 +1,15 @@
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
+ buildFirstMinute,
8
+ listUserVisibleWork,
9
+ personName,
10
+ speakFirstMinute,
11
+ speakKeepWorkingMinute,
12
+ } = require('../lib/first-minute');
4
13
  const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
5
14
 
6
15
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -241,6 +250,20 @@ function recapSoftTitle(title, maxWords = 5) {
241
250
  return `"${text.toLowerCase()}"`;
242
251
  }
243
252
 
253
+ function hasLiveKeepWorkingRun(root = process.cwd()) {
254
+ try {
255
+ const { pickLiveLocalMission } = require('./mission');
256
+ return Boolean(pickLiveLocalMission(root));
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+
262
+ function pickSpokenInProgress(items) {
263
+ const list = Array.isArray(items) ? items.filter(Boolean) : [];
264
+ return list.find((item) => item.owner) || list[0] || null;
265
+ }
266
+
244
267
  function renderRecapMinute(data, { person } = {}) {
245
268
  const who = person != null ? person : personName();
246
269
  const greet = who ? `hey ${who}, ` : '';
@@ -290,9 +313,15 @@ function renderRecapMinute(data, { person } = {}) {
290
313
  }
291
314
 
292
315
  if (inProgress.length) {
293
- const item = inProgress[0];
316
+ const item = pickSpokenInProgress(inProgress);
294
317
  const named = recapSoftTitle(item && item.title);
295
- if (item && item.owner && named) return `${greet}${named} is already yours.`;
318
+ if (item && item.owner && named) {
319
+ return [
320
+ `${greet}${named} is already yours.`,
321
+ '',
322
+ 'next: atris do',
323
+ ].join('\n');
324
+ }
296
325
  if (named) return `${greet}${named} is ready to claim.`;
297
326
  }
298
327
 
@@ -319,9 +348,42 @@ function recapAtris(args = []) {
319
348
  printRecapHelp();
320
349
  return;
321
350
  }
351
+ const root = process.cwd();
352
+ const visibleBefore = listUserVisibleWork(root);
322
353
  const daysIdx = args.indexOf('--days');
323
354
  const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
324
- const data = buildRecapData(process.cwd(), { days });
355
+ const data = buildRecapData(root, { days });
356
+ if (
357
+ data.empty
358
+ && isFreshWorkspace(root)
359
+ && !args.includes('--verbose')
360
+ && !args.includes('--full')
361
+ && !args.includes('--share')
362
+ ) {
363
+ return speakFirstMinute({
364
+ root,
365
+ asJson: args.includes('--json'),
366
+ files: visibleBefore,
367
+ });
368
+ }
369
+ // Just-minted file folder, nothing running: same two lines as
370
+ // first-minute / status / the next do. Not factory MAP.md.
371
+ // A claimed non-seed task still recaps that work. A live mission
372
+ // and --verbose / --share keep the recap report.
373
+ if (
374
+ !args.includes('--verbose')
375
+ && !args.includes('--full')
376
+ && !args.includes('--share')
377
+ && !hasLiveKeepWorkingRun(root)
378
+ ) {
379
+ const minute = buildFirstMinute({ root, files: visibleBefore });
380
+ if (isKeepWorkingMinute(minute)) {
381
+ return speakKeepWorkingMinute({
382
+ root,
383
+ asJson: args.includes('--json'),
384
+ });
385
+ }
386
+ }
325
387
  if (args.includes('--json')) {
326
388
  console.log(JSON.stringify(data, null, 2));
327
389
  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,
@@ -13,17 +13,42 @@
13
13
  * atris spaceship --hours 4 --repo /path/to/repo --interval 780 --yes
14
14
  * atris spaceship --hours 0.01 --tick-cmd /tmp/stub.sh --no-email --yes
15
15
  *
16
- * Without --yes, spaceship only prints the plan (no email, no overnight run).
16
+ * Without --yes, spaceship only talks the keep-working plan (no email, no run).
17
17
  * `--json` without --yes prints a JSON refuse and still does not start.
18
+ * `--yes` from an unbound scratch folder refuses: that folder is not a room.
18
19
  */
19
20
 
20
21
  const path = require('path');
21
22
  const fs = require('fs');
22
23
  const { spawn } = require('child_process');
23
24
  const { hasYesFlag, argsWantHelp, wantsJson } = require('../lib/noninteractive');
25
+ const { isUnboundScratchFolder, refuseUnboundScratch } = require('../lib/scratch-root');
26
+ const { resolveWorkspaceRoot } = require('../lib/mission-root');
27
+ const { personName } = require('../lib/first-minute');
24
28
 
25
29
  const SCRIPT = path.join(__dirname, '..', 'scripts', 'spaceship.sh');
26
30
 
31
+ function greet(person) {
32
+ return person ? `hey ${person}, ` : '';
33
+ }
34
+
35
+ function readFlagValue(args, name) {
36
+ const list = Array.isArray(args) ? args : [];
37
+ const idx = list.indexOf(name);
38
+ const raw = idx !== -1 && list[idx + 1] && !String(list[idx + 1]).startsWith('-')
39
+ ? String(list[idx + 1]).trim()
40
+ : '';
41
+ return raw;
42
+ }
43
+
44
+ function spokenHours(raw) {
45
+ const text = raw === undefined || raw === null ? '4' : String(raw).trim();
46
+ const n = Number(text);
47
+ if (!Number.isFinite(n) || n <= 0) return '4 hours';
48
+ if (n === 1) return `${text} hour`;
49
+ return `${text} hours`;
50
+ }
51
+
27
52
  function printUsage() {
28
53
  const lines = [
29
54
  'Usage: atris spaceship [--hours N] [--interval SEC] [--repo PATH]',
@@ -31,25 +56,34 @@ function printUsage() {
31
56
  ' [--idle-alert N] [--halt-alert N] [--label NAME]',
32
57
  ' [--no-email] [--yes]',
33
58
  '',
34
- 'Bounded overnight runner. Survives bad ticks and emails on state changes.',
35
- 'Without --yes, prints the plan only (no email, no overnight run).',
36
- 'Defaults: --hours 4, --interval 780, tick = atris autopilot --auto --iterations=1',
59
+ "Keep working here for a few hours. I'll write you if something changes.",
60
+ 'Without --yes, I only say the plan. --no-email stays quiet.',
37
61
  ];
38
62
  console.log(lines.join('\n'));
39
63
  }
40
64
 
41
65
  function planLines(args = []) {
42
- const hoursIdx = args.indexOf('--hours');
43
- const hours = hoursIdx !== -1 && args[hoursIdx + 1] ? args[hoursIdx + 1] : '4';
66
+ const list = Array.isArray(args) ? args : [];
67
+ const hours = spokenHours(readFlagValue(list, '--hours') || '4');
68
+ const next = list.includes('--no-email')
69
+ ? 'next: atris spaceship --yes'
70
+ : "I'll write you if something changes. next: atris spaceship --yes";
44
71
  return [
45
- 'spaceship plan (no run):',
46
- ` budget: ${hours}h`,
47
- ' email: on meaningful state changes (unless --no-email)',
48
- ' tick: atris autopilot --auto --iterations=1',
49
- 'Pass --yes to start the overnight run.',
72
+ `${greet(personName())}I can keep working here for ${hours}.`,
73
+ '',
74
+ next,
50
75
  ];
51
76
  }
52
77
 
78
+ function spaceshipTargetRoot(args = []) {
79
+ const list = Array.isArray(args) ? args : [];
80
+ const idx = list.indexOf('--repo');
81
+ const raw = idx !== -1 && list[idx + 1] && !String(list[idx + 1]).startsWith('-')
82
+ ? path.resolve(list[idx + 1])
83
+ : process.cwd();
84
+ return resolveWorkspaceRoot(raw);
85
+ }
86
+
53
87
  function spaceship(args = []) {
54
88
  const list = Array.isArray(args) ? args : [];
55
89
  if (argsWantHelp(list) || list.includes('--help') || list.includes('-h')) {
@@ -75,6 +109,12 @@ function spaceship(args = []) {
75
109
  process.exit(2);
76
110
  }
77
111
 
112
+ // --yes starts the run. It is not a workspace unlock. An unbound
113
+ // scratch folder is not a room (same class as slack/gmail from scratch).
114
+ if (isUnboundScratchFolder(spaceshipTargetRoot(list))) {
115
+ process.exit(refuseUnboundScratch());
116
+ }
117
+
78
118
  const runArgs = list.filter((a) => a !== '--yes' && a !== '-y' && a !== '--json');
79
119
  return new Promise((resolve, reject) => {
80
120
  if (!fs.existsSync(SCRIPT)) {
@@ -99,4 +139,4 @@ function spaceship(args = []) {
99
139
  });
100
140
  }
101
141
 
102
- module.exports = { spaceship, SCRIPT, printUsage, planLines };
142
+ module.exports = { spaceship, SCRIPT, printUsage, planLines, spokenHours, spaceshipTargetRoot };
@@ -4,6 +4,13 @@ const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journa
4
4
  const { parseTodo, getTeamActivity } = require('../lib/todo');
5
5
  const { clarify } = require('../lib/autoland');
6
6
  const { checkoutBehindMessage } = require('../lib/checkout-sync');
7
+ const {
8
+ isFreshWorkspace,
9
+ isKeepWorkingMinute,
10
+ buildFirstMinute,
11
+ speakKeepWorkingMinute,
12
+ speakNothingRunning,
13
+ } = require('../lib/first-minute');
7
14
 
8
15
  // Box drawing helpers
9
16
  const W = 64; // inner width
@@ -122,18 +129,34 @@ function parseStatusTodo(todoFile) {
122
129
  }
123
130
  }
124
131
 
132
+ function hasLiveKeepWorkingRun(root = process.cwd()) {
133
+ try {
134
+ const { pickLiveLocalMission } = require('./mission');
135
+ return Boolean(pickLiveLocalMission(root));
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
125
141
  function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
126
- const targetDir = path.join(process.cwd(), 'atris');
142
+ // Fresh folder: empty talks first-talk. A file already here
143
+ // names that file, same as bare atris. Do not mint a room.
144
+ if (isFreshWorkspace()) {
145
+ process.exit(speakNothingRunning({ asJson: jsonMode }));
146
+ }
127
147
 
128
- if (!fs.existsSync(targetDir)) {
129
- if (jsonMode) {
130
- console.log(JSON.stringify({ error: 'atris/ folder not found' }));
131
- process.exit(1);
148
+ // Just-minted file folder, nothing running: same two lines as
149
+ // first-minute / the next do. A live mission still gets the board.
150
+ // --verbose keeps the factory dump because the operator asked for it.
151
+ if (!verbose && !hasLiveKeepWorkingRun()) {
152
+ const minute = buildFirstMinute({ root: process.cwd() });
153
+ if (isKeepWorkingMinute(minute)) {
154
+ process.exit(speakKeepWorkingMinute({ asJson: jsonMode }));
132
155
  }
133
- console.log('✗ atris/ folder not found. Run "atris init" first.');
134
- process.exit(1);
135
156
  }
136
157
 
158
+ const targetDir = path.join(process.cwd(), 'atris');
159
+
137
160
  // Load task board state.
138
161
  const todoFile = path.join(targetDir, 'TODO.md');
139
162
  const todo = parseStatusTodo(todoFile);
package/commands/task.js CHANGED
@@ -60,7 +60,18 @@ const {
60
60
  decisionMarkerFor,
61
61
  DECISION_REFUSE_REASON,
62
62
  } = require('../lib/task-decision');
63
- const { buildFirstMinute, deskNextCommand, personName, pickNext, taskCommand, taskNextCommand } = require('../lib/first-minute');
63
+ const {
64
+ buildFirstMinute,
65
+ deskNextCommand,
66
+ firstTalkCommand,
67
+ folderName,
68
+ personName,
69
+ pickNext,
70
+ speakFirstMinute,
71
+ taskCommand,
72
+ taskNextCommand,
73
+ } = require('../lib/first-minute');
74
+ const { loadContext } = require('../lib/state-detection');
64
75
 
65
76
  const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
66
77
  || process.env.USER
@@ -6166,6 +6177,23 @@ function renderTaskDesk(rows, refRows = rows) {
6166
6177
  }
6167
6178
 
6168
6179
  function cmdAdd(args) {
6180
+ const root = process.cwd();
6181
+ // Empty folder talks like bare atris. A leftover title is not a
6182
+ // task desk. After init, a title still files.
6183
+ if (isUninitializedTaskFolder(root)) {
6184
+ if (wantsJson(args)) {
6185
+ printJson({
6186
+ ok: false,
6187
+ action: 'none',
6188
+ command: firstTalkCommand(folderName(root)),
6189
+ task_id: null,
6190
+ projection_path: null,
6191
+ task: null,
6192
+ });
6193
+ return;
6194
+ }
6195
+ return speakFirstMinute({ root, fresh: true });
6196
+ }
6169
6197
  const pos = positional(args);
6170
6198
  const title = pos.join(' ').trim();
6171
6199
  if (!title) {
@@ -6476,9 +6504,19 @@ function cmdDay(args) {
6476
6504
 
6477
6505
  function cmdFirstMinute() {
6478
6506
  const root = process.cwd();
6507
+ const fresh = !fs.existsSync(path.join(root, 'atris'));
6508
+ let context = {};
6509
+ if (!fresh) {
6510
+ try {
6511
+ context = loadContext(root);
6512
+ } catch {
6513
+ context = {};
6514
+ }
6515
+ }
6479
6516
  const screen = buildFirstMinute({
6480
6517
  root,
6481
- fresh: !fs.existsSync(path.join(root, 'atris')),
6518
+ fresh,
6519
+ context,
6482
6520
  });
6483
6521
  console.log(screen.text);
6484
6522
  }
@@ -6809,10 +6847,12 @@ function createEndgameSeedTask(taskDb, db, seed, owner) {
6809
6847
  }
6810
6848
 
6811
6849
  function nextActionFromCommand(command) {
6812
- const match = String(command || '').trim().match(/^atris (?:task|mission) (\S+)/);
6850
+ const text = String(command || '').trim();
6851
+ const match = text.match(/^atris (?:task|mission) (\S+)/);
6813
6852
  const verb = match ? match[1] : '';
6814
- if (!verb || verb === 'new') return 'none';
6815
- return verb;
6853
+ if (verb && verb !== 'new') return verb;
6854
+ if (/^atris do\b/.test(text)) return 'do';
6855
+ return 'none';
6816
6856
  }
6817
6857
 
6818
6858
  function cmdNextTruth(args) {
@@ -6823,9 +6863,24 @@ function cmdNextTruth(args) {
6823
6863
  const workspaceRoot = taskDb.workspaceRoot();
6824
6864
  const rows = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
6825
6865
  const existingProj = readProjectionFile(workspaceRoot);
6866
+ const dbHasActionable = rows.some((task) => {
6867
+ const status = task && task.status;
6868
+ return status === 'open' || status === 'claimed' || status === 'review';
6869
+ });
6870
+ const projHasActionable = Boolean(
6871
+ existingProj
6872
+ && Array.isArray(existingProj.tasks)
6873
+ && existingProj.tasks.some((task) => {
6874
+ const status = task && task.status;
6875
+ return status === 'open' || status === 'claimed' || status === 'review';
6876
+ }),
6877
+ );
6826
6878
  let projection;
6827
6879
  let outPath;
6828
- if (rows.length === 0 && existingProj && Array.isArray(existingProj.tasks) && existingProj.tasks.length > 0) {
6880
+ if (!dbHasActionable && projHasActionable) {
6881
+ projection = existingProj;
6882
+ outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
6883
+ } else if (rows.length === 0 && existingProj && Array.isArray(existingProj.tasks) && existingProj.tasks.length > 0) {
6829
6884
  projection = existingProj;
6830
6885
  outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
6831
6886
  } else {
@@ -6894,9 +6949,9 @@ function cmdNext(args) {
6894
6949
  if (isUninitializedTaskFolder(root)) {
6895
6950
  if (wantsJson(args)) {
6896
6951
  printJson({
6897
- ok: true,
6898
- action: 'init',
6899
- command: 'atris init --minimal',
6952
+ ok: false,
6953
+ action: 'none',
6954
+ command: firstTalkCommand(folderName(root)),
6900
6955
  task_id: null,
6901
6956
  owner: String(flag(args, '--as') || personName() || DEFAULT_OWNER),
6902
6957
  scope: normalizeTaskQueueScope(taskQueueScopeFromArgs(args)),
@@ -6905,9 +6960,7 @@ function cmdNext(args) {
6905
6960
  });
6906
6961
  return;
6907
6962
  }
6908
- const screen = buildFirstMinute({ root, fresh: true });
6909
- console.log(screen.text);
6910
- return;
6963
+ return speakFirstMinute({ root, fresh: true });
6911
6964
  }
6912
6965
  if (!hasFlag(args, '--create-next')) return cmdNextTruth(args);
6913
6966
  const owner = flag(args, '--as') || DEFAULT_OWNER;
@@ -9779,16 +9832,17 @@ function appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count, ids
9779
9832
  function cmdReady(args) {
9780
9833
  const pos = positional(args);
9781
9834
  const id = pos[0];
9782
- if (!id) {
9783
- console.error('atris task ready: id required');
9835
+ const proofFlag = flag(args, '--proof');
9836
+ const verifyFlag = flag(args, '--verify');
9837
+ const resultFlag = textFlag(args, ['--result']);
9838
+ if (!id || (!proofFlag && !verifyFlag && !resultFlag)) {
9839
+ console.log('Usage: atris task ready <id> --proof "..." --result "<sentence>"');
9784
9840
  process.exit(2);
9785
9841
  }
9786
9842
  // Two ways to prove: --proof "<note>" (claimed, pattern-checked) or
9787
9843
  // --verify "<command>" which actually RUNS the command and gates ready on exit 0.
9788
9844
  // If --proof names npm test, node --test, or git diff --check, this process
9789
9845
  // must have run that command. A sentence that names one of those is a lie.
9790
- const proofFlag = flag(args, '--proof');
9791
- const verifyFlag = flag(args, '--verify');
9792
9846
  const proofUrl = textFlag(args, ['--proof-url']);
9793
9847
  const iFetched = hasFlag(args, '--i-fetched');
9794
9848
  if (proofUrl && !iFetched) {
@@ -2,7 +2,7 @@
2
2
  * atris terminal <business> <command...> [--timeout N]
3
3
  *
4
4
  * Run a shell command directly on a business EC2 workspace via the warm runner.
5
- * This is the load-bearing primitive for fast bulk ops one bash call beats
5
+ * This is the load-bearing primitive for fast bulk ops: one bash call beats
6
6
  * hundreds of rate-limited individual file API calls.
7
7
  *
8
8
  * SAFETY:
@@ -27,7 +27,7 @@ const path = require('path');
27
27
  const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
28
28
  const { apiRequestJson } = require('../utils/api');
29
29
  const { loadBusinesses, saveBusinesses } = require('./business');
30
- const { requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
30
+ const { looksLikeBusinessSlug, requireAccountBound, refuseAccountGlobal } = require('../lib/account-bound');
31
31
  const { argsWantHelp, wantsJson } = require('../lib/noninteractive');
32
32
 
33
33
  function sleep(ms) {
@@ -151,8 +151,8 @@ async function terminalAtris() {
151
151
  try { return JSON.parse(fs.readFileSync(bizFile, 'utf8')).slug; } catch { return null; }
152
152
  })();
153
153
 
154
- // If first arg is a single word with no shell metacharacters, it might be a slug
155
- const firstLooksLikeSlug = args[0] && /^[a-z0-9-]+$/i.test(args[0]) && !args[0].includes(' ');
154
+ // Flags like --json match a hyphenated word regex. Require a real slug first.
155
+ const firstLooksLikeSlug = looksLikeBusinessSlug(args[0]);
156
156
 
157
157
  if (firstLooksLikeSlug && args.length > 1) {
158
158
  slug = args[0];
@@ -161,7 +161,7 @@ async function terminalAtris() {
161
161
  slug = cwdSlug;
162
162
  command = args.join(' ');
163
163
  } else if (firstLooksLikeSlug && args.length === 1) {
164
- // First (and only) arg is a slug-shaped word — could be the slug with no command
164
+ // First and only arg is a slug-shaped word with no command.
165
165
  console.error('Missing command. Usage: atris terminal <business> <command>');
166
166
  process.exit(1);
167
167
  } else {