atris 3.56.1 → 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.
package/bin/atris.js CHANGED
@@ -211,7 +211,7 @@ function parseNaturalEntryArgs(args = []) {
211
211
  };
212
212
  }
213
213
 
214
- function applyRunnerFlags(args) {
214
+ function applyRunnerFlags(args, options = {}) {
215
215
  // --engine <name> is the operator-facing spelling of --runner-profile:
216
216
  // one flag rents a specific intelligence for this run.
217
217
  const engineFlag = readOptionArg(args, '--engine');
@@ -251,6 +251,9 @@ function applyRunnerFlags(args) {
251
251
  process.env.ATRIS_RUNNER_MODEL = runnerModel;
252
252
  process.env.ATRIS_CLAUDE_MODEL = runnerModel;
253
253
  }
254
+ // Talk-only and unbound scratch still validate --engine / --runner-profile.
255
+ // They skip this seed so resolveDefaultEngine cannot mint engines.json.
256
+ if (options.seed === false) return;
254
257
  // No per-run choice and no env: the workspace's saved engine
255
258
  // (.atris/engine.json, written by `atris engine <name>`) becomes the
256
259
  // profile for every loop spawn. For the loop commands themselves, fall all
@@ -632,6 +635,8 @@ function showHelpAll() {
632
635
  console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
633
636
  console.log(' experiments validate - Validate experiment packs');
634
637
  console.log(' experiments run <slug> - Execute a pack or record an Endstate receipt');
638
+ console.log(' experiments keep <slug> - Keep a pack only when measure.py moves 0 to 1');
639
+ console.log(' experiments revert <slug> - Run a pack reset.py after a refused keep');
635
640
  console.log(' experiments benchmark [m] - Run validate/runtime experiment benchmarks');
636
641
  console.log(' bench - run core benchmark gates');
637
642
  console.log('');
@@ -2442,7 +2447,14 @@ if (command === 'init') {
2442
2447
  process.exit(0);
2443
2448
  }
2444
2449
 
2445
- applyRunnerFlags(args);
2450
+ const { isUnboundScratchFolder } = require('../lib/scratch-root');
2451
+ const { runObjective, runMissionFront } = require('../commands/run-front');
2452
+ const runTalkOnly = !runObjective(args) && !hasYesFlag(args) && !args.includes('--legacy');
2453
+ // Flag validation always runs. The engine-registry seed is what mints
2454
+ // .atris/state/engines.json, so first-talk and unbound scratch skip only that.
2455
+ applyRunnerFlags(args, {
2456
+ seed: !runTalkOnly && !isUnboundScratchFolder(process.cwd()),
2457
+ });
2446
2458
 
2447
2459
  if (args.includes('--legacy')) {
2448
2460
  const legacyArgs = args.filter(a => a !== '--legacy');
@@ -2462,7 +2474,7 @@ if (command === 'init') {
2462
2474
  process.exit(1);
2463
2475
  });
2464
2476
  } else {
2465
- require('../commands/run-front').runMissionFront(args)
2477
+ runMissionFront(args)
2466
2478
  .then((code) => process.exit(code || 0))
2467
2479
  .catch((error) => {
2468
2480
  console.error(`\u2717 Run failed: ${error.message || error}`);
@@ -2486,8 +2498,8 @@ if (command === 'init') {
2486
2498
  if (!autopilotHelp && !autopilotControl && !autopilotJsonPlan && !autopilotTalkOnly && isUnboundScratchFolder(autopilotRoot)) {
2487
2499
  process.exit(refuseUnboundScratch());
2488
2500
  }
2489
- if (!autopilotHelp && !isUnboundScratchFolder(autopilotRoot)) {
2490
- applyRunnerFlags(args);
2501
+ if (!autopilotHelp) {
2502
+ applyRunnerFlags(args, { seed: !isUnboundScratchFolder(autopilotRoot) });
2491
2503
  }
2492
2504
 
2493
2505
  if (args.includes('--legacy')) {
package/commands/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
1
+ const { AGENT_TOKEN_EXPIRED_DETAIL, loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
2
2
  const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
3
3
  const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
4
4
  const { hasFlag, readFlag } = require('../lib/arg-parser');
@@ -281,6 +281,11 @@ async function mintAgentToken(args = [], deps = {}) {
281
281
  async function printWhoamiPayload(asJson) {
282
282
  const ensured = await ensureValidCredentials(apiRequestJson);
283
283
  if (ensured.error) {
284
+ if (!asJson && ensured.detail === AGENT_TOKEN_EXPIRED_DETAIL) {
285
+ console.error('the agent key expired.');
286
+ console.error('atris login --agent');
287
+ process.exit(1);
288
+ }
284
289
  if (asJson) {
285
290
  console.log(JSON.stringify({
286
291
  ok: false,
@@ -25,6 +25,7 @@ const {
25
25
  buildRunnerCommand,
26
26
  } = require('../lib/runner-command');
27
27
  const { parseScopeFlag } = require('../lib/cli-scope');
28
+ const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
28
29
  const {
29
30
  ENGINE_ROLES,
30
31
  ENGINE_DUTIES,
@@ -1541,6 +1542,9 @@ function engineCommand(args = [], deps = {}) {
1541
1542
  }
1542
1543
 
1543
1544
  if (sub === 'chart' || scope.args.includes('--chart') || args.includes('--chart')) {
1545
+ if (isFreshWorkspace(root)) {
1546
+ return speakFirstMinute({ root, fresh: true, asJson: json });
1547
+ }
1544
1548
  printEngineChart(root);
1545
1549
  return 0;
1546
1550
  }
@@ -1551,6 +1555,9 @@ function engineCommand(args = [], deps = {}) {
1551
1555
  console.log('\n atris engine roster + current default\n atris engines --chart show the fleet as an org chart\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
1552
1556
  return 0;
1553
1557
  }
1558
+ if (isFreshWorkspace(root)) {
1559
+ return speakFirstMinute({ root, fresh: true, asJson: json });
1560
+ }
1554
1561
  if (json) {
1555
1562
  console.log(JSON.stringify(registryPayload(root, { scope: scope.kind }), null, 2));
1556
1563
  return 0;
@@ -138,6 +138,176 @@ function runPython(scriptPath, args = [], cwd = process.cwd()) {
138
138
  }
139
139
  }
140
140
 
141
+ function parseMeasureScore(stdout) {
142
+ const line = String(stdout || '').trim().split(/\r?\n/).filter(Boolean).pop();
143
+ if (!line) return { ok: false, error: 'measure.py printed no score' };
144
+ try {
145
+ const payload = JSON.parse(line);
146
+ const score = Number(payload.score);
147
+ if (!Number.isFinite(score)) return { ok: false, error: 'measure.py printed no score' };
148
+ return { ok: true, score, payload };
149
+ } catch {
150
+ return { ok: false, error: 'measure.py printed no score' };
151
+ }
152
+ }
153
+
154
+ function runMeasureJson(scriptPath, workspaceDir = process.cwd()) {
155
+ const python = resolvePython();
156
+ if (!python) {
157
+ return { ok: false, error: 'python not found. set ATRIS_EXPERIMENTS_PYTHON or install python3.' };
158
+ }
159
+
160
+ const result = spawnSync(python, [scriptPath], {
161
+ cwd: workspaceDir,
162
+ encoding: 'utf8',
163
+ env: {
164
+ ...process.env,
165
+ PYTHONDONTWRITEBYTECODE: '1',
166
+ ATRIS_REPO_ROOT: workspaceDir,
167
+ },
168
+ });
169
+
170
+ if (result.error) {
171
+ return { ok: false, error: result.error.message || 'measure.py failed to start' };
172
+ }
173
+
174
+ const parsed = parseMeasureScore(result.stdout);
175
+ if (!parsed.ok) {
176
+ const detail = String(result.stderr || '').trim().split(/\r?\n/).filter(Boolean).pop();
177
+ return { ok: false, error: detail || parsed.error };
178
+ }
179
+ return parsed;
180
+ }
181
+
182
+ function experimentsKeep(name) {
183
+ const token = String(name || '').trim();
184
+ if (!token || token === '--help' || token === '-h' || token === 'help') {
185
+ console.error('usage: atris experiments keep <slug>');
186
+ process.exit(2);
187
+ }
188
+ if (!SLUG_RE.test(token)) {
189
+ console.error('invalid experiment name. use a lowercase-hyphen slug.');
190
+ process.exit(2);
191
+ }
192
+
193
+ const workspaceDir = process.cwd();
194
+ if (!fs.existsSync(path.join(workspaceDir, 'atris'))) {
195
+ console.error('atris/ folder not found. run "atris init" first.');
196
+ process.exit(2);
197
+ }
198
+
199
+ const packDir = path.join(workspaceDir, 'atris', 'experiments', token);
200
+ if (!fs.existsSync(packDir) || !fs.statSync(packDir).isDirectory()) {
201
+ console.error(`experiment "${token}" not found.`);
202
+ process.exit(2);
203
+ }
204
+
205
+ const measurePath = path.join(packDir, 'measure.py');
206
+ if (!fs.existsSync(measurePath)) {
207
+ console.error(`experiment "${token}" has no measure.py.`);
208
+ process.exit(2);
209
+ }
210
+
211
+ // Change is already on disk. Minted packs start at 0; keep only if current score is 1.
212
+ const measured = runMeasureJson(measurePath, workspaceDir);
213
+ if (!measured.ok) {
214
+ console.error(`revert ${token}: ${measured.error}. refuse keep.`);
215
+ process.exit(1);
216
+ }
217
+
218
+ if (measured.score === 1) {
219
+ console.log(`keep ${token}: measure.py moved 0→1`);
220
+ return;
221
+ }
222
+
223
+ const stayed = measured.score === 0 ? '0' : String(measured.score);
224
+ console.error(`revert ${token}: measure.py stayed ${stayed}. refuse keep.`);
225
+ process.exit(1);
226
+ }
227
+
228
+ function lastProcessLine(result) {
229
+ const text = `${result && result.stderr ? result.stderr : ''}\n${result && result.stdout ? result.stdout : ''}`;
230
+ return text.trim().split(/\r?\n/).filter(Boolean).pop() || '';
231
+ }
232
+
233
+ function runResetScript(scriptPath, workspaceDir = process.cwd()) {
234
+ const python = resolvePython();
235
+ if (!python) {
236
+ return { ok: false, error: 'python not found. set ATRIS_EXPERIMENTS_PYTHON or install python3.' };
237
+ }
238
+
239
+ const result = spawnSync(python, [scriptPath], {
240
+ cwd: workspaceDir,
241
+ encoding: 'utf8',
242
+ env: {
243
+ ...process.env,
244
+ PYTHONDONTWRITEBYTECODE: '1',
245
+ ATRIS_REPO_ROOT: workspaceDir,
246
+ },
247
+ });
248
+
249
+ if (result.error) {
250
+ return { ok: false, error: result.error.message || 'reset.py failed to start' };
251
+ }
252
+
253
+ if (typeof result.status === 'number' && result.status !== 0) {
254
+ return { ok: false, error: lastProcessLine(result) || `reset.py exited ${result.status}` };
255
+ }
256
+
257
+ if (result.status == null) {
258
+ return { ok: false, error: lastProcessLine(result) || 'reset.py failed' };
259
+ }
260
+
261
+ return { ok: true };
262
+ }
263
+
264
+ function experimentsRevert(name) {
265
+ const token = String(name || '').trim();
266
+ if (!token || token === '--help' || token === '-h' || token === 'help') {
267
+ console.error('usage: atris experiments revert <slug>');
268
+ process.exit(2);
269
+ }
270
+ if (!SLUG_RE.test(token)) {
271
+ console.error('invalid experiment name. use a lowercase-hyphen slug.');
272
+ process.exit(2);
273
+ }
274
+
275
+ const workspaceDir = process.cwd();
276
+ if (!fs.existsSync(path.join(workspaceDir, 'atris'))) {
277
+ console.error('atris/ folder not found. run "atris init" first.');
278
+ process.exit(2);
279
+ }
280
+
281
+ const packDir = path.join(workspaceDir, 'atris', 'experiments', token);
282
+ if (!fs.existsSync(packDir) || !fs.statSync(packDir).isDirectory()) {
283
+ console.error(`experiment "${token}" not found.`);
284
+ process.exit(2);
285
+ }
286
+
287
+ const resetPath = path.join(packDir, 'reset.py');
288
+ if (!fs.existsSync(resetPath)) {
289
+ console.error(`experiment "${token}" has no reset.py.`);
290
+ process.exit(2);
291
+ }
292
+
293
+ const reset = runResetScript(resetPath, workspaceDir);
294
+ if (!reset.ok) {
295
+ console.error(`revert ${token}: ${reset.error}. refuse revert.`);
296
+ process.exit(1);
297
+ }
298
+
299
+ const measurePath = path.join(packDir, 'measure.py');
300
+ if (fs.existsSync(measurePath)) {
301
+ const measured = runMeasureJson(measurePath, workspaceDir);
302
+ if (measured.ok) {
303
+ console.log(`revert ${token}: reset.py ran. measure.py ${measured.score}`);
304
+ return;
305
+ }
306
+ }
307
+
308
+ console.log(`revert ${token}: reset.py ran`);
309
+ }
310
+
141
311
  function experimentsInit(name) {
142
312
  const { experimentsDir } = ensureExperimentsFramework();
143
313
 
@@ -573,6 +743,10 @@ function experimentsCommand(subcommand, ...args) {
573
743
  return experimentsReplay(args[0] || 'endstate');
574
744
  case 'run':
575
745
  return experimentsRun(args[0], ...args.slice(1));
746
+ case 'keep':
747
+ return experimentsKeep(args[0]);
748
+ case 'revert':
749
+ return experimentsRevert(args[0]);
576
750
  default:
577
751
  console.log('');
578
752
  console.log('Usage: atris experiments <subcommand> [name]');
@@ -581,6 +755,8 @@ function experimentsCommand(subcommand, ...args) {
581
755
  console.log(' init [slug] Prepare atris/experiments/ or scaffold a new pack');
582
756
  console.log(' validate [path|slug] Run structural validation on packs or a single pack');
583
757
  console.log(' run <slug> Execute a pack or record an Endstate benchmark receipt');
758
+ console.log(' keep <slug> Keep a pack only when measure.py moves 0 to 1');
759
+ console.log(' revert <slug> Run a pack reset.py to restore the working tree');
584
760
  console.log(' compare endstate Compare the latest baseline and stack receipts');
585
761
  console.log(' replay endstate Validate, dry-run, and compare the public benchmark flow');
586
762
  console.log(' benchmark [mode] Run validate/runtime/all benchmark harness');
@@ -592,6 +768,8 @@ function experimentsCommand(subcommand, ...args) {
592
768
  console.log(' atris experiments init self-heal');
593
769
  console.log(' atris experiments validate');
594
770
  console.log(' atris experiments run endstate-baseline --dry-run');
771
+ console.log(' atris experiments keep teach-teach01-s1');
772
+ console.log(' atris experiments revert teach-teach01-s1');
595
773
  console.log(' atris experiments compare endstate');
596
774
  console.log(' atris experiments replay endstate');
597
775
  console.log(' atris experiments benchmark runtime');
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { spawnSync } = require('node:child_process');
6
+ const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
6
7
 
7
8
  const DAY_MS = 24 * 60 * 60 * 1000;
8
9
  const DEFAULT_DAYS = 28;
@@ -321,6 +322,17 @@ function founderCommand(args = [], options = {}) {
321
322
 
322
323
  const cwd = path.resolve(options.cwd || process.cwd());
323
324
  const currentWorkspace = workspaceRoot(cwd);
325
+
326
+ // Empty folder talks like bare atris. Do not write scorecard.jsonl.
327
+ // After init, founder still lists and scores.
328
+ if (isFreshWorkspace(cwd) || isFreshWorkspace(currentWorkspace)) {
329
+ return speakFirstMinute({
330
+ root: cwd,
331
+ fresh: true,
332
+ asJson: parsed.json,
333
+ });
334
+ }
335
+
324
336
  const root = parsed.root
325
337
  ? path.resolve(cwd, parsed.root)
326
338
  : (parsed.global ? defaultScanRoot(cwd, currentWorkspace) : currentWorkspace);
@@ -14,7 +14,15 @@ const {
14
14
  const { apiRequestJson } = require('../utils/api');
15
15
  const { decodeJwtClaims, loadCredentials } = require('../utils/auth');
16
16
  const { isHelpToken } = require('../lib/noninteractive');
17
- const { isFreshWorkspace, speakFirstMinute, speakNothingRunning } = require('../lib/first-minute');
17
+ const {
18
+ buildFirstMinute,
19
+ isClaimMinute,
20
+ isFreshWorkspace,
21
+ isKeepWorkingMinute,
22
+ speakFirstMinute,
23
+ speakKeepWorkingMinute,
24
+ speakNothingRunning,
25
+ } = require('../lib/first-minute');
18
26
 
19
27
  const PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
20
28
  const HUMAN_STATES = Object.freeze({
@@ -168,8 +176,10 @@ function parseMissionId(args = []) {
168
176
 
169
177
  /**
170
178
  * Scratch / unbound folders must not drive account-current cloud work.
171
- * Empty-folder stop talks first-talk instead. After a room exists,
172
- * require an explicit mission/task id: atris stop --mission <id>
179
+ * Empty-folder stop talks first-talk instead. After do/claim, nothing
180
+ * running talks keep-working instead. After init, claim-ready talks
181
+ * the claim instead. After a room exists without that keep-working
182
+ * or claim next, require an explicit id: atris stop --mission <id>
173
183
  */
174
184
  function refuseUnboundCloudComputer(args = [], options = {}, commandName = 'stop') {
175
185
  const root = options.root || process.cwd();
@@ -739,6 +749,15 @@ async function approveCommand(args, options = {}) {
739
749
  return changeCurrentMission('approve', {}, args, options);
740
750
  }
741
751
 
752
+ function hasLiveKeepWorkingRun(root = process.cwd()) {
753
+ try {
754
+ const { pickLiveLocalMission } = require('./mission');
755
+ return Boolean(pickLiveLocalMission(root));
756
+ } catch {
757
+ return false;
758
+ }
759
+ }
760
+
742
761
  async function stopCommand(args, options = {}) {
743
762
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
744
763
  (options.log || console.log)('Usage: atris stop [--mission <id>] [--json]');
@@ -746,16 +765,35 @@ async function stopCommand(args, options = {}) {
746
765
  }
747
766
  const root = options.root || process.cwd();
748
767
  const asJson = args.includes('--json');
768
+ const speak = {
769
+ root,
770
+ asJson,
771
+ log: options.log || console.log,
772
+ };
749
773
  // Fresh folder: empty talks first-talk. A file already here
750
774
  // names that file, same as bare atris. Do not mint a room.
751
775
  if (isFreshWorkspace(root) && !parseMissionId(args)) {
752
776
  const unbound = refuseUnboundCloudComputer(args, options, 'stop');
753
777
  if (unbound) {
754
- return speakNothingRunning({
755
- root,
756
- asJson,
757
- log: options.log || console.log,
758
- });
778
+ return speakNothingRunning(speak);
779
+ }
780
+ }
781
+ // After do/claim, nothing running: same two keep-working
782
+ // lines as bare atris / status / recap. After init, next
783
+ // is claim: same two lines as bare atris / status / now.
784
+ // Not factory cloud-computer. Do not mint a room or bind
785
+ // a computer. A bound computer still stops the current
786
+ // mission.
787
+ if (!parseMissionId(args) && !hasLiveKeepWorkingRun(root)) {
788
+ const unbound = refuseUnboundCloudComputer(args, options, 'stop');
789
+ if (unbound) {
790
+ const minute = buildFirstMinute({ root });
791
+ if (isKeepWorkingMinute(minute)) {
792
+ return speakKeepWorkingMinute(speak);
793
+ }
794
+ if (isClaimMinute(minute)) {
795
+ return speakFirstMinute(speak);
796
+ }
759
797
  }
760
798
  }
761
799
  return changeCurrentMission('stop', {}, args, options);
@@ -3,6 +3,14 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { hasFlag, readFlag } = require('../lib/arg-parser');
6
+ const {
7
+ isFreshWorkspace,
8
+ isKeepWorkingMinute,
9
+ isClaimMinute,
10
+ buildFirstMinute,
11
+ speakFirstMinute,
12
+ speakKeepWorkingMinute,
13
+ } = require('../lib/first-minute');
6
14
 
7
15
  const ACTIVE_MISSION_STATUSES = new Set(['planning', 'running', 'ready']);
8
16
 
@@ -185,15 +193,18 @@ function loadTaskProjection(root) {
185
193
  };
186
194
  }
187
195
 
188
- function chooseNextAction({ root, actor, tasks, missions, brain, endgame }) {
189
- if (!fs.existsSync(path.join(root, 'atris'))) {
190
- return {
191
- kind: 'bootstrap',
192
- label: 'Initialize this workspace',
193
- command: 'atris init',
194
- why: 'No atris/ directory exists yet.',
195
- };
196
+ function hasLiveKeepWorkingRun(root = process.cwd()) {
197
+ try {
198
+ const { pickLiveLocalMission } = require('./mission');
199
+ return Boolean(pickLiveLocalMission(root));
200
+ } catch {
201
+ return false;
196
202
  }
203
+ }
204
+
205
+ function chooseNextAction({ actor, tasks, missions, brain, endgame }) {
206
+ // No-room folders belong to first-minute, not factory init.
207
+ // launchpadCommand speaks those two lines before this ranking runs.
197
208
 
198
209
  const ownClaimed = newest(tasks.filter(task => (
199
210
  task.status === 'claimed'
@@ -609,8 +620,32 @@ function launchpadCommand(args = []) {
609
620
  showLaunchpadHelp();
610
621
  return 0;
611
622
  }
612
- const payload = collectLaunchpad(process.cwd(), args);
613
- if (hasFlag(args, '--json')) {
623
+ const asJson = hasFlag(args, '--json');
624
+ const root = process.cwd();
625
+
626
+ // Fresh folder: empty talks first-talk. A file already here
627
+ // names that file, same as bare atris. Do not mint a room.
628
+ if (isFreshWorkspace(root)) {
629
+ return speakFirstMinute({ root, fresh: true, asJson });
630
+ }
631
+
632
+ // Just-minted file folder, nothing running: same two lines as
633
+ // first-minute / the next keep-working ready. After init, next is claim: same
634
+ // two lines as bare atris / status / now. Not factory init.
635
+ // A live mission still gets the board. --json on the claim
636
+ // path keeps the factory card for scripts.
637
+ if (!hasLiveKeepWorkingRun(root)) {
638
+ const minute = buildFirstMinute({ root });
639
+ if (isKeepWorkingMinute(minute)) {
640
+ return speakKeepWorkingMinute({ root, asJson });
641
+ }
642
+ if (!asJson && isClaimMinute(minute)) {
643
+ return speakFirstMinute({ root });
644
+ }
645
+ }
646
+
647
+ const payload = collectLaunchpad(root, args);
648
+ if (asJson) {
614
649
  console.log(JSON.stringify(payload, null, 2));
615
650
  } else {
616
651
  process.stdout.write(renderLaunchpad(payload));
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');
@@ -589,8 +596,16 @@ function printNowJson(payload) {
589
596
  console.log(JSON.stringify(payload, null, 2));
590
597
  }
591
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
+
592
608
  function nowJsonPayload(root = process.cwd()) {
593
- const { buildFirstMinute, isFreshWorkspace } = require('../lib/first-minute');
594
609
  const fresh = isFreshWorkspace(root);
595
610
  const screen = buildFirstMinute({ root, fresh });
596
611
  const payload = { ok: !fresh };
@@ -630,6 +645,26 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
630
645
  return payload.ok ? 0 : 2;
631
646
  }
632
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
+ }
666
+ }
667
+
633
668
  let result;
634
669
  if (all) {
635
670
  const workspaces = findChildWorkspaces(root);
package/commands/recap.js CHANGED
@@ -4,11 +4,13 @@ const {
4
4
  isCertifiedReview,
5
5
  isFreshWorkspace,
6
6
  isKeepWorkingMinute,
7
+ isClaimMinute,
7
8
  buildFirstMinute,
8
9
  listUserVisibleWork,
9
10
  personName,
10
11
  speakFirstMinute,
11
12
  speakKeepWorkingMinute,
13
+ taskCommand,
12
14
  } = require('../lib/first-minute');
13
15
  const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
14
16
 
@@ -319,7 +321,7 @@ function renderRecapMinute(data, { person } = {}) {
319
321
  return [
320
322
  `${greet}${named} is already yours.`,
321
323
  '',
322
- 'next: atris do',
324
+ `next: ${taskCommand({ display_id: item.id, status: 'claimed' })}`,
323
325
  ].join('\n');
324
326
  }
325
327
  if (named) return `${greet}${named} is ready to claim.`;
@@ -348,7 +350,22 @@ function recapAtris(args = []) {
348
350
  printRecapHelp();
349
351
  return;
350
352
  }
351
- const root = process.cwd();
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
+ }
352
369
  const visibleBefore = listUserVisibleWork(root);
353
370
  const daysIdx = args.indexOf('--days');
354
371
  const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
@@ -356,6 +373,7 @@ function recapAtris(args = []) {
356
373
  if (
357
374
  data.empty
358
375
  && isFreshWorkspace(root)
376
+ && !inProjectSubdir
359
377
  && !args.includes('--verbose')
360
378
  && !args.includes('--full')
361
379
  && !args.includes('--share')
@@ -367,13 +385,18 @@ function recapAtris(args = []) {
367
385
  });
368
386
  }
369
387
  // Just-minted file folder, nothing running: same two lines as
370
- // first-minute / status / the next do. Not factory MAP.md.
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.
371
393
  // A claimed non-seed task still recaps that work. A live mission
372
394
  // and --verbose / --share keep the recap report.
373
395
  if (
374
396
  !args.includes('--verbose')
375
397
  && !args.includes('--full')
376
398
  && !args.includes('--share')
399
+ && !inProjectSubdir
377
400
  && !hasLiveKeepWorkingRun(root)
378
401
  ) {
379
402
  const minute = buildFirstMinute({ root, files: visibleBefore });
@@ -383,6 +406,14 @@ function recapAtris(args = []) {
383
406
  asJson: args.includes('--json'),
384
407
  });
385
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
+ }
386
417
  }
387
418
  if (args.includes('--json')) {
388
419
  console.log(JSON.stringify(data, null, 2));