atris 3.74.0 → 3.74.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 (59) hide show
  1. package/bin/atris.js +85 -82
  2. package/commands/aeo.js +2 -2
  3. package/commands/align.js +10 -19
  4. package/commands/auth.js +1 -1
  5. package/commands/autopilot.js +9 -10
  6. package/commands/avail.js +36 -7
  7. package/commands/clean.js +2 -1
  8. package/commands/close.js +7 -4
  9. package/commands/computer.js +3 -1
  10. package/commands/console.js +1 -1
  11. package/commands/context-sync.js +1 -60
  12. package/commands/experiments.js +4 -1
  13. package/commands/fleet.js +0 -1
  14. package/commands/improve.js +3 -1
  15. package/commands/interview.js +13 -5
  16. package/commands/land.js +5 -1
  17. package/commands/lifecycle.js +17 -6
  18. package/commands/log-sync.js +1 -1
  19. package/commands/loops.js +5 -2
  20. package/commands/member.js +17 -11
  21. package/commands/mission.js +1 -4
  22. package/commands/now.js +0 -7
  23. package/commands/play.js +3 -1
  24. package/commands/plugin.js +1 -1
  25. package/commands/pull.js +5 -3
  26. package/commands/push.js +0 -1
  27. package/commands/radar.js +0 -4
  28. package/commands/recap.js +25 -1
  29. package/commands/report.js +0 -4
  30. package/commands/review.js +1 -1
  31. package/commands/security-review.js +4 -1
  32. package/commands/setup.js +6 -2
  33. package/commands/soul.js +2 -1
  34. package/commands/sync.js +8 -2
  35. package/commands/task.js +0 -522
  36. package/commands/terminal.js +1 -1
  37. package/commands/x-search.js +5 -3
  38. package/commands/youtube.js +9 -7
  39. package/commands/zen.js +4 -4
  40. package/lib/agent-today.js +412 -0
  41. package/lib/apply-gate.js +5 -3
  42. package/lib/autoland.js +0 -8
  43. package/lib/bench/runner.js +0 -4
  44. package/lib/chat-log-scan.js +0 -28
  45. package/lib/cli-scope.js +4 -2
  46. package/lib/engine-registry.js +0 -4
  47. package/lib/experiments/daily.js +2 -1
  48. package/lib/file-ops.js +6 -1
  49. package/lib/first-minute.js +2 -0
  50. package/lib/gm-bet.js +73 -9
  51. package/lib/gm-phone-page.js +42 -5
  52. package/lib/gm-phone.js +3 -0
  53. package/lib/local-day.js +10 -0
  54. package/lib/manifest.js +0 -10
  55. package/lib/member-switches.js +0 -11
  56. package/lib/review-integrity.js +0 -4
  57. package/lib/scorecard.js +7 -6
  58. package/lib/wiki.js +4 -1
  59. package/package.json +1 -1
package/commands/align.js CHANGED
@@ -42,15 +42,6 @@ function sleep(ms) {
42
42
  return new Promise((r) => setTimeout(r, ms));
43
43
  }
44
44
 
45
- /**
46
- * Compute SHA-256 hash of content string.
47
- * @param {string} content - Content to hash
48
- * @returns {string} Hex-encoded hash
49
- */
50
- function hashContent(content) {
51
- return crypto.createHash('sha256').update(content).digest('hex');
52
- }
53
-
54
45
  /**
55
46
  * Compute SHA-256 hash of a file.
56
47
  * @param {string} absPath - Absolute path to file
@@ -472,19 +463,19 @@ async function alignAtris() {
472
463
  const printUsage = () => {
473
464
  console.log('Usage: atris align [business] [--fix] [--hard] [--from cloud|local] [--dry-run]');
474
465
  console.log('');
475
- console.log(' atris align Diff current workspace against cloud (auto-detect)');
476
- console.log(' atris align example-co Diff example-co workspace');
477
- console.log(' atris align example-co --fix Fix drift (local is canonical by default)');
478
- console.log(' atris align example-co --fix --hard Force-push: nuke cloud cruft, upload local. Skips diff. Fast.');
479
- console.log(' atris align example-co --fix --from cloud Cloud is canonical: pull EC2-only, delete local extras');
480
- console.log(' atris align example-co --dry-run Show what would change, do nothing');
466
+ console.log(' atris align Show what differs between this folder and its cloud copy');
467
+ console.log(' atris align example-co Show what differs for the example-co workspace');
468
+ console.log(' atris align example-co --fix Make them match, keeping this computer\'s version');
469
+ console.log(' atris align example-co --fix --hard Make the cloud an exact copy of this computer: delete cloud-only files, upload the rest. Fast.');
470
+ console.log(' atris align example-co --fix --from cloud Keep the cloud version: copy cloud-only files down, delete extra local files');
471
+ console.log(' atris align example-co --dry-run Show what would change, change nothing');
481
472
  };
482
473
 
483
474
  // Explicit help must win before we try to auto-detect a business slug,
484
475
  // otherwise `--help` gets overwritten by the .atris/business.json fallback
485
476
  // and the command attempts a real align instead of printing usage.
486
- const firstArg = process.argv[3];
487
- if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') {
477
+ const alignArgs = process.argv.slice(3);
478
+ if (alignArgs[0] === 'help' || alignArgs.includes('--help') || alignArgs.includes('-h')) {
488
479
  printUsage();
489
480
  process.exit(0);
490
481
  }
@@ -515,7 +506,7 @@ async function alignAtris() {
515
506
  }
516
507
 
517
508
  if (hard && fromSide !== 'local') {
518
- console.error('--hard only supported with --from local (the canonical force-push direction).');
509
+ console.error('--hard only works with --from local: it overwrites the cloud with this computer\'s files, never the other way.');
519
510
  process.exit(1);
520
511
  }
521
512
 
@@ -546,7 +537,7 @@ async function alignAtris() {
546
537
  // Wake EC2 (the rule)
547
538
  const endpoint = await ensureAwake(creds.token, biz.businessId);
548
539
  if (!endpoint) {
549
- console.error(' EC2 computer did not become ready in time. Aborting.');
540
+ console.error(` The cloud computer for "${slug}" did not wake up in time, so nothing ran. Wait a minute, then run the same command again.`);
550
541
  process.exit(1);
551
542
  }
552
543
 
package/commands/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, loadCredentials, saveCredentials, saveAgentClientCredentials, preserveAgentClientLogin, 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, decodeJwtClaims, loadCredentials, saveCredentials, saveAgentClientCredentials, preserveAgentClientLogin, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
2
2
  const { getAppBaseUrl, getApiBaseUrl, apiRequestJson } = require('../utils/api');
3
3
  const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
4
4
  const { hasFlag, readFlag } = require('../lib/arg-parser');
@@ -1157,13 +1157,19 @@ function lessonSlug(text, maxLen = 40) {
1157
1157
  return trimmed.replace(/-+$/g, '') || 'unknown';
1158
1158
  }
1159
1159
 
1160
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
1161
+ // already tomorrow in the evening anywhere west of Greenwich.
1162
+ function localDay(date = new Date()) {
1163
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
1164
+ }
1165
+
1160
1166
  /**
1161
1167
  * Write a lesson to atris/lessons.md
1162
1168
  * Appends a line in format: - **[YYYY-MM-DD] slug** - pass/fail - explanation
1163
1169
  */
1164
1170
  function writeLesson(cwd, slug, status, explanation) {
1165
1171
  const lessonsPath = path.join(cwd, 'atris', 'lessons.md');
1166
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
1172
+ const today = localDay();
1167
1173
  const lessonLine = `- **[${today}] ${slug}** - ${status} - ${explanation}`;
1168
1174
 
1169
1175
  if (!fs.existsSync(lessonsPath)) {
@@ -2959,7 +2965,7 @@ function markLessonAttempted(cwd, slug, reason) {
2959
2965
  if (!meta[slug] || typeof meta[slug] !== 'object') meta[slug] = {};
2960
2966
  meta[slug].status = 'attempted';
2961
2967
  meta[slug].attempts = (typeof meta[slug].attempts === 'number' ? meta[slug].attempts : 0) + 1;
2962
- meta[slug].last_attempt = new Date().toISOString().slice(0, 10);
2968
+ meta[slug].last_attempt = localDay();
2963
2969
  if (reason) meta[slug].last_attempt_reason = String(reason);
2964
2970
  try {
2965
2971
  const atrisDir = path.join(cwd, 'atris');
@@ -3181,7 +3187,7 @@ function hasRecentVerifyPrePass(cwd, taskTitle, windowDays = 7) {
3181
3187
  if (lessons.length === 0) return false;
3182
3188
  const cutoff = new Date();
3183
3189
  cutoff.setDate(cutoff.getDate() - windowDays);
3184
- const cutoffDate = cutoff.toISOString().split('T')[0];
3190
+ const cutoffDate = localDay(cutoff);
3185
3191
  const needle = `"${taskTitle}"`;
3186
3192
  for (const l of lessons) {
3187
3193
  if (l.id !== 'verify-not-falsifiable') continue;
@@ -4043,13 +4049,6 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
4043
4049
  }
4044
4050
  }
4045
4051
 
4046
- /**
4047
- * Entry point when called without a description.
4048
- */
4049
- async function autopilotFromTodo(options = {}) {
4050
- return autopilotAtris(null, options);
4051
- }
4052
-
4053
4052
  module.exports = {
4054
4053
  askModel,
4055
4054
  snapshotRepoHeads,
package/commands/avail.js CHANGED
@@ -117,6 +117,21 @@ function resolveTimezone({ tz, where }) {
117
117
  return 'America/Los_Angeles';
118
118
  }
119
119
 
120
+ function parseWholeNumber(raw, min, max) {
121
+ if (raw === undefined || !/^\d+$/.test(String(raw).trim())) return null;
122
+ const n = parseInt(raw, 10);
123
+ if (n < min || n > max) return null;
124
+ return n;
125
+ }
126
+
127
+ function requireWholeNumber(flag, raw, min, max) {
128
+ const n = parseWholeNumber(raw, min, max);
129
+ if (n === null) {
130
+ throw new Error(`${flag} needs a whole number from ${min} to ${max}, got: ${raw === undefined ? 'nothing' : raw}`);
131
+ }
132
+ return n;
133
+ }
134
+
120
135
  function parseSetArgs(argv) {
121
136
  const opts = {
122
137
  wake: null,
@@ -138,10 +153,10 @@ function parseSetArgs(argv) {
138
153
  else if (a === '--tz') opts.tz = argv[++i];
139
154
  else if (a === '--where') opts.where = argv[++i];
140
155
  else if (a === '--days') opts.days = argv[++i];
141
- else if (a === '--slot-minutes') opts.slotMinutes = parseInt(argv[++i], 10);
142
- else if (a === '--window-days') opts.windowDays = parseInt(argv[++i], 10);
156
+ else if (a === '--slot-minutes') opts.slotMinutes = requireWholeNumber(a, argv[++i], 5, 480);
157
+ else if (a === '--window-days') opts.windowDays = requireWholeNumber(a, argv[++i], 1, 365);
143
158
  else if (a === '--help' || a === '-h') return { help: true };
144
- else throw new Error(`unknown arg: ${a}`);
159
+ else throw new Error(`atris avail set does not take "${a}". Run: atris avail --help`);
145
160
  }
146
161
  if (opts.wake == null || opts.sleep == null) {
147
162
  throw new Error('--wake and --sleep are required (e.g. --wake 2:30pm --sleep 9pm)');
@@ -287,18 +302,30 @@ async function setAvail(argv) {
287
302
  return 0;
288
303
  }
289
304
 
305
+ function parseSlotsDays(raw) {
306
+ return parseWholeNumber(raw, 1, 365);
307
+ }
308
+
290
309
  async function slotsAvail(argv) {
291
310
  const opts = { days: 7, username: null, json: false };
292
311
  for (let i = 0; i < argv.length; i += 1) {
293
312
  const a = argv[i];
294
313
  if (a === '--json') opts.json = true;
295
- else if (a === '--days') opts.days = parseInt(argv[++i], 10);
314
+ else if (a === '--days') {
315
+ const raw = argv[++i];
316
+ const days = parseSlotsDays(raw);
317
+ if (days === null) {
318
+ console.error(`--days needs a whole number from 1 to 365, got: ${raw === undefined ? 'nothing' : raw}`);
319
+ return 1;
320
+ }
321
+ opts.days = days;
322
+ }
296
323
  else if (a === '--user' || a === '--username') opts.username = argv[++i];
297
324
  else if (a === '--help' || a === '-h') {
298
325
  printHelp();
299
326
  return 0;
300
327
  } else {
301
- console.error(`unknown arg: ${a}`);
328
+ console.error(`atris avail slots does not take "${a}". Run: atris avail --help`);
302
329
  return 1;
303
330
  }
304
331
  }
@@ -320,7 +347,7 @@ async function slotsAvail(argv) {
320
347
  try {
321
348
  data = JSON.parse(result.body.toString('utf8'));
322
349
  } catch {
323
- console.error('Invalid slots response');
350
+ console.error(`the booking server sent back a reply atris could not read (HTTP ${result.status}). Try again in a minute.`);
324
351
  return 1;
325
352
  }
326
353
  if (result.status < 200 || result.status >= 300) {
@@ -392,7 +419,7 @@ async function availCommand(argv = []) {
392
419
  const sub = argv[0].toLowerCase();
393
420
  if (sub === 'set' || sub === 'update') return setAvail(argv.slice(1));
394
421
  if (sub === 'slots') return slotsAvail(argv.slice(1));
395
- console.error(`Unknown subcommand: ${sub}`);
422
+ console.error(`atris avail does not know "${sub}". Use show, set, or slots.`);
396
423
  printHelp();
397
424
  return 1;
398
425
  }
@@ -402,6 +429,8 @@ module.exports = {
402
429
  formatBookingLink,
403
430
  parseTimeToDecimal,
404
431
  parseDaysArg,
432
+ parseSlotsDays,
433
+ parseSetArgs,
405
434
  buildAvailableHours,
406
435
  resolveTimezone,
407
436
  formatDecimalHour,
package/commands/clean.js CHANGED
@@ -3,6 +3,7 @@ const os = require('os');
3
3
  const path = require('path');
4
4
  const { spawnSync } = require('child_process');
5
5
  const escapeRegExp = require('../lib/escape-regexp');
6
+ const { localDay } = require('../lib/local-day');
6
7
  const { wikiMetabolismNudge } = require('../lib/wiki');
7
8
 
8
9
  /**
@@ -723,7 +724,7 @@ function checkPageStaleness(cwd, filePath) {
723
724
  page: filePath,
724
725
  staleSource: source,
725
726
  compiledDate: compiledMatch[1],
726
- sourceDate: changed.toISOString().split('T')[0]
727
+ sourceDate: localDay(changed)
727
728
  };
728
729
  }
729
730
  } catch {
package/commands/close.js CHANGED
@@ -85,8 +85,10 @@ function appendEvent(event, cwd = process.cwd()) {
85
85
  fs.appendFileSync(ledgerPath(cwd), `${JSON.stringify(event)}\n`);
86
86
  }
87
87
 
88
- function isoDateOnly(date) {
89
- return date.toISOString().slice(0, 10);
88
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
89
+ // already tomorrow in the evening anywhere west of Greenwich.
90
+ function localDay(date) {
91
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
90
92
  }
91
93
 
92
94
  function parseDate(value) {
@@ -156,7 +158,8 @@ function foldEvents(events, options = {}) {
156
158
  delete flag.parked_at;
157
159
  delete flag.why;
158
160
  } else if (event.kind === 'escalated') {
159
- const day = event.day || (event.at ? String(event.at).slice(0, 10) : null);
161
+ const at = event.at ? new Date(event.at) : null;
162
+ const day = event.day || (at && Number.isFinite(at.getTime()) ? localDay(at) : null);
160
163
  if (day && !flag.escalated_days.includes(day)) flag.escalated_days.push(day);
161
164
  } else if (event.kind === 'probed') {
162
165
  flag.probe_runs.push({
@@ -753,7 +756,7 @@ function commandSnooze(args, context = {}) {
753
756
  // Escalation core shared by the CLI verb and the hourly pulse tick: append
754
757
  // one escalated event per overdue flag per day, return the resulting state.
755
758
  function sweepState(cwd = process.cwd(), now = new Date(), options = {}) {
756
- const today = isoDateOnly(now);
759
+ const today = localDay(now);
757
760
  const autoClosed = autoCloseResolvedTaskReviewFlags(cwd, now);
758
761
  const autoParked = migrateOpenSlots(cwd, now, options);
759
762
  let flags = openFlags(cwd, { now });
@@ -35,7 +35,7 @@ const {
35
35
  runEngineDeviceLoginCommand,
36
36
  validEngineLoginSeat,
37
37
  } = require('./engine');
38
- const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
38
+ const { consoleCommand } = require('./console');
39
39
  const { streamSession } = require('./serve');
40
40
  const { buildRemoteAtrisBootstrapCommand } = require('../lib/runtime-bootstrap');
41
41
  const { envForcesNonInteractive } = require('../lib/noninteractive');
@@ -4380,6 +4380,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4380
4380
  if (!codeopsCtx) {
4381
4381
  console.error('Atris CodeOps is not available for this account.');
4382
4382
  console.error('Ask an Atris CodeOps admin to add you to the atris-codeops business.');
4383
+ process.exitCode = 1;
4383
4384
  return;
4384
4385
  }
4385
4386
  const codeopsOptions = codeOpsCloudOptions(cloudOptions);
@@ -4418,6 +4419,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4418
4419
  default:
4419
4420
  console.error(`Unknown CodeOps subcommand: ${codeopsSub}`);
4420
4421
  console.log('Run: atris computer codeops help');
4422
+ process.exitCode = 1;
4421
4423
  return;
4422
4424
  }
4423
4425
  return;
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const { spawn, spawnSync } = require('child_process');
4
+ const { spawnSync } = require('child_process');
5
5
  const readline = require('readline');
6
6
  const { resolveClaudeRunnerBin } = require('../lib/runner-command');
7
7
 
@@ -222,65 +222,6 @@ async function businessDiff(slug) {
222
222
  }
223
223
 
224
224
 
225
- /**
226
- * atris log <business-slug>
227
- * Shows human-readable commit history.
228
- */
229
- async function businessLog(slug) {
230
- const biz = await resolveBusiness(slug);
231
-
232
- if (!biz.workspaceId) {
233
- console.error(`Business "${slug}" has no workspace.`);
234
- process.exit(1);
235
- }
236
-
237
- const limit = 20;
238
- const pathFilter = process.argv.includes('--path') ? process.argv[process.argv.indexOf('--path') + 1] : null;
239
-
240
- const params = `limit=${limit}` + (pathFilter ? `&path=${encodeURIComponent(pathFilter)}` : '');
241
- const result = await apiRequestJson(
242
- `/business/${biz.businessId}/workspaces/${biz.workspaceId}/git/log?${params}`,
243
- { method: 'GET', token: biz.token }
244
- );
245
-
246
- if (!result.ok) {
247
- if (result.status === 409) {
248
- console.log('\n Computer is sleeping. Wake it first.\n');
249
- } else {
250
- console.error(`\n Failed to get history: ${result.errorMessage || result.status}\n`);
251
- }
252
- process.exit(1);
253
- }
254
-
255
- const commits = result.data.commits || [];
256
- if (commits.length === 0) {
257
- console.log(`\n ${biz.name}: no history yet.\n`);
258
- return;
259
- }
260
-
261
- console.log(`\n ${biz.name}: history\n`);
262
-
263
- for (const commit of commits) {
264
- const date = _timeSince(commit.date) || commit.date;
265
- const msg = commit.message || '';
266
-
267
- // Parse actor from message format "actor/name: message"
268
- const colonIdx = msg.indexOf(': ');
269
- let actor = '';
270
- let description = msg;
271
- if (colonIdx > 0 && colonIdx < 40) {
272
- actor = msg.substring(0, colonIdx);
273
- description = msg.substring(colonIdx + 2);
274
- }
275
-
276
- const actorDisplay = actor ? ` ${actor}` : '';
277
- console.log(` ${date.padEnd(12)} ${actorDisplay}`);
278
- console.log(` ${description}`);
279
- console.log('');
280
- }
281
- }
282
-
283
-
284
225
  /**
285
226
  * Convert ISO timestamp to human-readable relative time (e.g., "5m ago").
286
227
  * @param {string} isoString - ISO 8601 timestamp
@@ -300,4 +241,4 @@ function _timeSince(isoString) {
300
241
  }
301
242
 
302
243
 
303
- module.exports = { businessStatus, businessDiff, businessLog };
244
+ module.exports = { businessStatus, businessDiff };
@@ -600,7 +600,10 @@ function buildBenchmarkArtifact(name, packDir, options) {
600
600
  function recordExperimentRun(experimentId) {
601
601
  const root = process.cwd();
602
602
  const state = daily.readDailyState(root);
603
- const today = new Date().toISOString().slice(0, 10);
603
+ // Local calendar day; toISOString is UTC and records an evening run as
604
+ // tomorrow's anywhere west of Greenwich.
605
+ const now = new Date();
606
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
604
607
  state.last_run_date = today;
605
608
  if (!Array.isArray(state.history)) state.history = [];
606
609
  if (!state.history.includes(experimentId)) {
package/commands/fleet.js CHANGED
@@ -18,7 +18,6 @@ const https = require('https');
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
20
  const os = require('os');
21
- const crypto = require('crypto');
22
21
  const { parseScopeFlag } = require('../lib/cli-scope');
23
22
 
24
23
  // ── Config ──────────────────────────────────────────────
@@ -25,6 +25,7 @@ const path = require('path');
25
25
  const { spawnSync } = require('child_process');
26
26
  const gitSpawn = require('../lib/git-spawn');
27
27
  const ignoredCache = require('../lib/ignored-cache');
28
+ const { localDay } = require('../lib/local-day');
28
29
  const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
29
30
  const { loadCredentials } = require('../utils/auth');
30
31
  const pulse = require('../lib/pulse');
@@ -375,7 +376,7 @@ function agePhrase(ts, nowMs = Date.now()) {
375
376
  if (delta < 60 * 60 * 1000) return `${plural(Math.round(delta / (60 * 1000)), 'minute')} ago`;
376
377
  if (delta < DAY_MS) return `${plural(Math.round(delta / (60 * 60 * 1000)), 'hour')} ago`;
377
378
  if (delta < 30 * DAY_MS) return `${plural(Math.round(delta / DAY_MS), 'day')} ago`;
378
- return `on ${new Date(ms).toISOString().slice(0, 10)}`;
379
+ return `on ${localDay(new Date(ms))}`;
379
380
  }
380
381
 
381
382
  function plainSentence(value) {
@@ -1715,6 +1716,7 @@ async function run(argv = [], deps = {}) {
1715
1716
 
1716
1717
  module.exports = {
1717
1718
  run,
1719
+ agePhrase,
1718
1720
  runImprove,
1719
1721
  runImproveCore,
1720
1722
  parseImproveArgs,
@@ -27,9 +27,17 @@ function findWorkspaceRoot(start) {
27
27
  return null;
28
28
  }
29
29
 
30
+ // The local calendar day (YYYY-MM-DD), offsetDays back. toISOString() is the
31
+ // UTC day, which is already tomorrow in the evening west of Greenwich.
30
32
  function today(offsetDays = 0) {
31
- const d = new Date(Date.now() - offsetDays * 86400000);
32
- return d.toISOString().slice(0, 10);
33
+ const d = new Date();
34
+ d.setDate(d.getDate() - offsetDays);
35
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
36
+ }
37
+
38
+ function journalPath(ws, offsetDays = 0) {
39
+ const day = today(offsetDays);
40
+ return path.join(ws, 'atris', 'logs', day.slice(0, 4), `${day}.md`);
33
41
  }
34
42
 
35
43
  function existing(...candidates) {
@@ -178,8 +186,8 @@ async function interviewCommand(args = [], root = process.cwd(), { mode: forcedM
178
186
  if (subjectRaw === 'me' || subjectRaw === '') {
179
187
  mode = 'operator';
180
188
  contextPaths = existing(
181
- path.join(ws, 'atris', 'logs', '2026', `${today()}.md`),
182
- path.join(ws, 'atris', 'logs', '2026', `${today(1)}.md`),
189
+ journalPath(ws),
190
+ journalPath(ws, 1),
183
191
  path.join(ws, 'atris', 'now.md'),
184
192
  path.join(ws, 'atris', 'TODO.md')
185
193
  );
@@ -200,7 +208,7 @@ async function interviewCommand(args = [], root = process.cwd(), { mode: forcedM
200
208
  path.join(memberDir, 'now.md'),
201
209
  path.join(memberDir, 'logs'),
202
210
  path.join(memberDir, 'proofs'),
203
- path.join(ws, 'atris', 'logs', '2026', `${today()}.md`)
211
+ journalPath(ws)
204
212
  );
205
213
  } else {
206
214
  mode = 'expert';
package/commands/land.js CHANGED
@@ -473,7 +473,10 @@ function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
473
473
  }
474
474
 
475
475
  function salvageDir(root) {
476
- const stamp = new Date().toISOString().slice(0, 10);
476
+ // Local calendar day; toISOString is UTC and files an evening backup
477
+ // under tomorrow anywhere west of Greenwich.
478
+ const now = new Date();
479
+ const stamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
477
480
  const dir = path.join(root, '.atris', 'salvage', stamp);
478
481
  fs.mkdirSync(dir, { recursive: true });
479
482
  return dir;
@@ -1086,5 +1089,6 @@ module.exports = {
1086
1089
  landCommand,
1087
1090
  landSummary,
1088
1091
  reap,
1092
+ salvageDir,
1089
1093
  sharedBoard,
1090
1094
  };
@@ -11,6 +11,15 @@ const {
11
11
  setMemberLoopAwake,
12
12
  } = require('../lib/member-switches');
13
13
 
14
+ /**
15
+ * True when the user asked for help. Checked before anything else so
16
+ * `atris sleep --help` inside a bound folder never sleeps that business.
17
+ * @returns {boolean}
18
+ */
19
+ function wantsHelp() {
20
+ return process.argv.slice(3).some((a) => a === '--help' || a === '-h' || a === 'help');
21
+ }
22
+
14
23
  /**
15
24
  * Resolve business slug from CLI arg or local .atris/business.json.
16
25
  * @returns {string|null} The resolved slug, or null if not found.
@@ -121,11 +130,12 @@ async function waitForBusinessComputer(token, business, maxWait = 90000) {
121
130
  * @returns {Promise<void>}
122
131
  */
123
132
  async function sleepAtris() {
124
- if (handleMemberSwitch(false)) return;
133
+ const help = wantsHelp();
134
+ if (!help && handleMemberSwitch(false)) return;
125
135
 
126
- const slug = resolveSlug();
136
+ const slug = help ? null : resolveSlug();
127
137
 
128
- if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
138
+ if (!slug) {
129
139
  console.log('Usage: atris sleep [business|member] [--loop <loop-id>]');
130
140
  console.log('');
131
141
  console.log(' Pause a workspace to save compute. Storage only.');
@@ -172,11 +182,12 @@ async function sleepAtris() {
172
182
  * @returns {Promise<void>}
173
183
  */
174
184
  async function wakeAtris() {
175
- if (handleMemberSwitch(true)) return;
185
+ const help = wantsHelp();
186
+ if (!help && handleMemberSwitch(true)) return;
176
187
 
177
- const slug = resolveSlug();
188
+ const slug = help ? null : resolveSlug();
178
189
 
179
- if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
190
+ if (!slug) {
180
191
  console.log('Usage: atris wake [business|member] [--loop <loop-id>]');
181
192
  console.log('');
182
193
  console.log(' Wake a sleeping workspace. Wake signal sent; loops pick it up on their next pass.');
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/file-ops');
3
+ const { getLogPath, createLogFile } = require('../lib/file-ops');
4
4
  const { parseJournalSections, mergeSections, reconstructJournal, showLogDiff } = require('../lib/journal');
5
5
  const { computeContentHash, isSameTimestamp } = require('../lib/journal');
6
6
  const { loadConfig, loadLogSyncState, saveLogSyncState } = require('../utils/config');
package/commands/loops.js CHANGED
@@ -160,7 +160,7 @@ function setRegistryJob(id, enabled) {
160
160
  delete job.disabled_reason;
161
161
  } else {
162
162
  job.disabled = true;
163
- job.disabled_reason = `stopped via atris loops ${new Date().toISOString().slice(0, 10)}`;
163
+ job.disabled_reason = `stopped via atris loops ${todayIso()}`;
164
164
  }
165
165
  saveRegistry(registry);
166
166
  return true;
@@ -204,8 +204,11 @@ function controlLoop(id, action) {
204
204
  return 0;
205
205
  }
206
206
 
207
+ // Local calendar day (YYYY-MM-DD). toISOString() is the UTC day, which is
208
+ // already tomorrow in the evening anywhere west of Greenwich.
207
209
  function todayIso() {
208
- return new Date().toISOString().slice(0, 10);
210
+ const d = new Date();
211
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
209
212
  }
210
213
 
211
214
  function renderTemplate(text) {
@@ -4513,13 +4513,15 @@ function memberList(...flags) {
4513
4513
 
4514
4514
  console.log('');
4515
4515
  console.log('Team Members');
4516
- console.log('─'.repeat(70));
4516
+ console.log('─'.repeat(72));
4517
4517
 
4518
4518
  const nameW = 16;
4519
- const roleW = 16;
4520
- const fmtW = 6;
4519
+ const roleW = 18;
4520
+ const fmtW = 8;
4521
4521
  const skillW = 8;
4522
- const ctxW = 8;
4522
+ const ctxW = 9;
4523
+ // Keep one space between columns, even when a value fills its column.
4524
+ const cell = (value, width) => `${clipCell(value, width - 1)} `;
4523
4525
 
4524
4526
  console.log(
4525
4527
  ' ' +
@@ -4530,18 +4532,18 @@ function memberList(...flags) {
4530
4532
  'Context'.padEnd(ctxW) +
4531
4533
  'Version'
4532
4534
  );
4533
- console.log(' ' + '─'.repeat(66));
4535
+ console.log(' ' + '─'.repeat(70));
4534
4536
 
4535
4537
  for (const m of members) {
4536
4538
  const skills = m.format === 'directory' ? String(m.skillCount) : '-';
4537
4539
  const context = m.format === 'directory' ? String(m.contextCount) : '-';
4538
4540
  console.log(
4539
4541
  ' ' +
4540
- clipCell(m.name, nameW) +
4541
- clipCell(m.role, roleW) +
4542
- clipCell(m.format === 'directory' ? 'dir' : 'flat', fmtW) +
4543
- clipCell(skills, skillW) +
4544
- clipCell(context, ctxW) +
4542
+ cell(m.name, nameW) +
4543
+ cell(m.role, roleW) +
4544
+ cell(m.format === 'directory' ? 'dir' : 'flat', fmtW) +
4545
+ cell(skills, skillW) +
4546
+ cell(context, ctxW) +
4545
4547
  (m.version || '-') +
4546
4548
  (parked.has(m.name) ? ' parked' : '')
4547
4549
  );
@@ -9887,7 +9889,11 @@ async function memberImprovements(businessId, ...args) {
9887
9889
  console.error('Not logged in. Run: atris login');
9888
9890
  process.exit(1);
9889
9891
  }
9890
- const days = flags.days && Number.isFinite(flags.days) ? flags.days : 30;
9892
+ if (flags.days !== null && !(Number.isInteger(flags.days) && flags.days >= 1)) {
9893
+ console.error('--days needs a whole number of days, 1 or more (example: --days 7).');
9894
+ process.exit(1);
9895
+ }
9896
+ const days = flags.days || 30;
9891
9897
  const result = await apiRequestJson(`/business/${encodeURIComponent(businessId)}/improvements/daily?days=${days}`, { token: creds.token });
9892
9898
  if (!result.ok) {
9893
9899
  console.error(`Failed to fetch improvements: ${result.error || 'Unknown error'}`);
@@ -38,6 +38,7 @@ const {
38
38
  resolveEngineForRoleWithPreference,
39
39
  engineFailureHealthStatus,
40
40
  engineStallReason,
41
+ engineRegistryView,
41
42
  recordEngineRunHealth,
42
43
  setEngineHealth,
43
44
  } = require('../lib/engine-registry');
@@ -3862,10 +3863,6 @@ function chooseNextMissionAnalysis(mission, root = process.cwd()) {
3862
3863
  return { target: null, near_misses: [] };
3863
3864
  }
3864
3865
 
3865
- function chooseNextMissionTarget(mission, root = process.cwd()) {
3866
- return chooseNextMissionAnalysis(mission, root).target;
3867
- }
3868
-
3869
3866
  function normalizeMissionOwner(value) {
3870
3867
  return String(value || '').trim().toLowerCase();
3871
3868
  }
package/commands/now.js CHANGED
@@ -273,13 +273,6 @@ function taskReceiptTitle(row) {
273
273
  return compactLine(row?.state?.title || row?.title || row?.task_id || 'untitled work');
274
274
  }
275
275
 
276
- // The receipts behind "Completed receipts today", as lines a human can read:
277
- // one per receipt, landed result + human proof state. Shares the collector
278
- // with the count so the number and the visible list always match.
279
- function todayTaskReceiptLines(root = process.cwd(), date = new Date(), limit = 6) {
280
- return formatTaskReceiptLines(collectTaskReceiptsToday(root, date), limit);
281
- }
282
-
283
276
  // Format an already-collected receipt list, so a caller that needs both the
284
277
  // count and the lines (renderDefaultNow) parses the receipt files only once.
285
278
  function formatTaskReceiptLines(receipts, limit = 6) {