atris 3.56.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/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, speakFirstMinute, 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
@@ -6172,9 +6183,9 @@ function cmdAdd(args) {
6172
6183
  if (isUninitializedTaskFolder(root)) {
6173
6184
  if (wantsJson(args)) {
6174
6185
  printJson({
6175
- ok: true,
6176
- action: 'init',
6177
- command: 'atris init --minimal',
6186
+ ok: false,
6187
+ action: 'none',
6188
+ command: firstTalkCommand(folderName(root)),
6178
6189
  task_id: null,
6179
6190
  projection_path: null,
6180
6191
  task: null,
@@ -6493,9 +6504,19 @@ function cmdDay(args) {
6493
6504
 
6494
6505
  function cmdFirstMinute() {
6495
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
+ }
6496
6516
  const screen = buildFirstMinute({
6497
6517
  root,
6498
- fresh: !fs.existsSync(path.join(root, 'atris')),
6518
+ fresh,
6519
+ context,
6499
6520
  });
6500
6521
  console.log(screen.text);
6501
6522
  }
@@ -6826,10 +6847,12 @@ function createEndgameSeedTask(taskDb, db, seed, owner) {
6826
6847
  }
6827
6848
 
6828
6849
  function nextActionFromCommand(command) {
6829
- 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+)/);
6830
6852
  const verb = match ? match[1] : '';
6831
- if (!verb || verb === 'new') return 'none';
6832
- return verb;
6853
+ if (verb && verb !== 'new') return verb;
6854
+ if (/^atris do\b/.test(text)) return 'do';
6855
+ return 'none';
6833
6856
  }
6834
6857
 
6835
6858
  function cmdNextTruth(args) {
@@ -6840,9 +6863,24 @@ function cmdNextTruth(args) {
6840
6863
  const workspaceRoot = taskDb.workspaceRoot();
6841
6864
  const rows = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
6842
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
+ );
6843
6878
  let projection;
6844
6879
  let outPath;
6845
- 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) {
6846
6884
  projection = existingProj;
6847
6885
  outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
6848
6886
  } else {
@@ -6911,9 +6949,9 @@ function cmdNext(args) {
6911
6949
  if (isUninitializedTaskFolder(root)) {
6912
6950
  if (wantsJson(args)) {
6913
6951
  printJson({
6914
- ok: true,
6915
- action: 'init',
6916
- command: 'atris init --minimal',
6952
+ ok: false,
6953
+ action: 'none',
6954
+ command: firstTalkCommand(folderName(root)),
6917
6955
  task_id: null,
6918
6956
  owner: String(flag(args, '--as') || personName() || DEFAULT_OWNER),
6919
6957
  scope: normalizeTaskQueueScope(taskQueueScopeFromArgs(args)),
@@ -6922,9 +6960,7 @@ function cmdNext(args) {
6922
6960
  });
6923
6961
  return;
6924
6962
  }
6925
- const screen = buildFirstMinute({ root, fresh: true });
6926
- console.log(screen.text);
6927
- return;
6963
+ return speakFirstMinute({ root, fresh: true });
6928
6964
  }
6929
6965
  if (!hasFlag(args, '--create-next')) return cmdNextTruth(args);
6930
6966
  const owner = flag(args, '--as') || DEFAULT_OWNER;
@@ -9796,16 +9832,17 @@ function appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count, ids
9796
9832
  function cmdReady(args) {
9797
9833
  const pos = positional(args);
9798
9834
  const id = pos[0];
9799
- if (!id) {
9800
- 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>"');
9801
9840
  process.exit(2);
9802
9841
  }
9803
9842
  // Two ways to prove: --proof "<note>" (claimed, pattern-checked) or
9804
9843
  // --verify "<command>" which actually RUNS the command and gates ready on exit 0.
9805
9844
  // If --proof names npm test, node --test, or git diff --check, this process
9806
9845
  // must have run that command. A sentence that names one of those is a lie.
9807
- const proofFlag = flag(args, '--proof');
9808
- const verifyFlag = flag(args, '--verify');
9809
9846
  const proofUrl = textFlag(args, ['--proof-url']);
9810
9847
  const iFetched = hasFlag(args, '--i-fetched');
9811
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 {
@@ -6,13 +6,16 @@ const {
6
6
  folderName,
7
7
  freshMinuteJson,
8
8
  isCertifiedReview,
9
+ listUserVisibleWork,
9
10
  isFreshWorkspace,
10
11
  personName,
11
12
  pickNext,
12
13
  renderWorkspace,
13
14
  speakFirstMinute,
14
15
  taskCommand,
16
+ visibleWorkTitle,
15
17
  } = require('../lib/first-minute');
18
+ const { startFirstTalk } = require('../lib/context-gatherer');
16
19
  const { isNonInteractive } = require('../lib/noninteractive');
17
20
  const { loadContext } = require('../lib/state-detection');
18
21
  const { buildToolResultBody } = require('../lib/tool-result-encode');
@@ -64,7 +67,8 @@ function reviewSoftTitle(title, maxWords = 5) {
64
67
  }
65
68
 
66
69
  function isHumanDeskNext(command) {
67
- return /^atris task (?:claim|ready|accept)\b/.test(String(command || ''));
70
+ const text = String(command || '');
71
+ return /^atris do\b/.test(text) || /^atris task (?:claim|show|ready|accept)\b/.test(text);
68
72
  }
69
73
 
70
74
  function loadReviewTasks(root = process.cwd()) {
@@ -506,7 +510,7 @@ async function planAtris(userInput = null) {
506
510
  // init --minimal is optional context, not a factory bounce.
507
511
  if (!fs.existsSync(targetDir)) {
508
512
  if (args.includes('--json')) {
509
- console.log(JSON.stringify(freshMinuteJson(), null, 2));
513
+ console.log(JSON.stringify(freshMinuteJson(folderName(cwd), listUserVisibleWork(cwd), { root: cwd }), null, 2));
510
514
  process.exit(2);
511
515
  }
512
516
  const screen = buildFirstMinute({ root: cwd, fresh: true });
@@ -877,11 +881,20 @@ async function doAtris() {
877
881
  const cwd = process.cwd();
878
882
  const targetDir = path.join(cwd, 'atris');
879
883
 
880
- // Empty folder talks like bare atris. Missing executor.md after
881
- // init --minimal is optional context, not a factory bounce.
884
+ // Empty folder talks like bare atris. Files already here start
885
+ // first-talk, then next is atris do. A second do stays two first-minute
886
+ // lines. Missing executor.md after init --minimal is optional
887
+ // context, not a factory bounce.
882
888
  if (!fs.existsSync(targetDir)) {
889
+ const visible = listUserVisibleWork(cwd);
890
+ if (visible.length) {
891
+ const title = visibleWorkTitle(visible, folderName(cwd));
892
+ const code = startFirstTalk(cwd, title, { asJson: args.includes('--json') });
893
+ if (code !== 0) process.exit(code);
894
+ return;
895
+ }
883
896
  if (args.includes('--json')) {
884
- console.log(JSON.stringify(freshMinuteJson(), null, 2));
897
+ console.log(JSON.stringify(freshMinuteJson(folderName(cwd), visible, { root: cwd }), null, 2));
885
898
  process.exit(2);
886
899
  }
887
900
  const screen = buildFirstMinute({ root: cwd, fresh: true });