atris 3.74.0 → 3.74.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/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');
@@ -1163,7 +1163,10 @@ function lessonSlug(text, maxLen = 40) {
1163
1163
  */
1164
1164
  function writeLesson(cwd, slug, status, explanation) {
1165
1165
  const lessonsPath = path.join(cwd, 'atris', 'lessons.md');
1166
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
1166
+ // Local calendar day (YYYY-MM-DD); toISOString is UTC and dates an
1167
+ // evening lesson tomorrow anywhere west of Greenwich.
1168
+ const now = new Date();
1169
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
1167
1170
  const lessonLine = `- **[${today}] ${slug}** - ${status} - ${explanation}`;
1168
1171
 
1169
1172
  if (!fs.existsSync(lessonsPath)) {
@@ -4043,13 +4046,6 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
4043
4046
  }
4044
4047
  }
4045
4048
 
4046
- /**
4047
- * Entry point when called without a description.
4048
- */
4049
- async function autopilotFromTodo(options = {}) {
4050
- return autopilotAtris(null, options);
4051
- }
4052
-
4053
4049
  module.exports = {
4054
4050
  askModel,
4055
4051
  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,
@@ -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 };
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 ──────────────────────────────────────────────
@@ -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');
@@ -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) {
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
- const { findAllSkills, parseFrontmatter } = require('./skill');
5
+ const { findAllSkills } = require('./skill');
6
6
 
7
7
  // --- Recursive Copy Helper ---
8
8
 
package/commands/pull.js CHANGED
@@ -7,7 +7,7 @@ const { loadConfig } = require('../utils/config');
7
7
  const { getLogPath } = require('../lib/file-ops');
8
8
  const { parseJournalSections, mergeSections, reconstructJournal } = require('../lib/journal');
9
9
  const { loadBusinesses, businessMatchesSlug } = require('./business');
10
- const { loadManifest, saveManifest, computeFileHash, buildManifest, computeLocalHashes, threeWayCompare, isIgnoredSyncPath } = require('../lib/manifest');
10
+ const { loadManifest, saveManifest, buildManifest, computeLocalHashes, threeWayCompare, isIgnoredSyncPath } = require('../lib/manifest');
11
11
  const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
12
12
  const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
13
13
  const { resolveSafeOutputDir } = require('../lib/workspace-safety');
package/commands/push.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const crypto = require('crypto');
4
3
  const readline = require('readline');
5
4
  const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
6
5
  const { apiRequestJson } = require('../utils/api');
package/commands/radar.js CHANGED
@@ -685,10 +685,6 @@ function taskBinding(source, scope = null) {
685
685
  return { task_source: source, task_scope: scope };
686
686
  }
687
687
 
688
- function taskBindingForProjection(task) {
689
- return task ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null);
690
- }
691
-
692
688
  function taskSourceLabel(sources = []) {
693
689
  if (sources.includes('member_loop_lock')) return 'member loop lock';
694
690
  if (sources.includes('mission_lock')) return 'mission lock';
package/commands/recap.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  taskCommand,
14
14
  } = require('../lib/first-minute');
15
15
  const { isRealTestRunnerProof, quoteVerifierCommand } = require('../lib/verifier-quality');
16
+ const { buildAgentToday, renderAgentToday, hasActivity, ranAgent } = require('../lib/agent-today');
16
17
 
17
18
  const DAY_MS = 24 * 60 * 60 * 1000;
18
19
  const DEFAULT_DAYS = 7;
@@ -38,7 +39,8 @@ function readProjection(root) {
38
39
 
39
40
  function loadTasks(root) {
40
41
  const taskDb = loadTaskDb();
41
- if (taskDb) {
42
+ // Reading never creates the db: a first run before any setup writes nothing.
43
+ if (taskDb && fs.existsSync(taskDb.getDbPath())) {
42
44
  try {
43
45
  const db = taskDb.open();
44
46
  const ws = taskDb.workspaceRoot(root);
@@ -335,11 +337,14 @@ function printRecapHelp() {
335
337
  atris recap - what got done, in spoken lines
336
338
 
337
339
  atris recap a few spoken lines: waiting for your ok, still being checked
340
+ atris recap today what your ai coding agent did in this folder today
338
341
  atris recap --verbose the old report
339
342
  atris recap --days 30 widen the window
340
343
  atris recap --share paste-ready summary for Slack, email, or a customer
341
344
  atris recap --json structured output for agents and dashboards
342
345
 
346
+ in a folder with no atris setup, recap shows today by default.
347
+ today reads claude code and codex logs on this computer plus git. it sends nothing.
343
348
  certified work names atris task accept. uncertified stays still being checked.
344
349
  use --verbose or --share for the old report.
345
350
  `);
@@ -351,6 +356,11 @@ function recapAtris(args = []) {
351
356
  return;
352
357
  }
353
358
  const cwd = process.cwd();
359
+ if (args[0] === 'today') {
360
+ const today = buildAgentToday({ cwd });
361
+ console.log(args.includes('--json') ? JSON.stringify(today, null, 2) : renderAgentToday(today));
362
+ return;
363
+ }
354
364
  const taskDb = loadTaskDb();
355
365
  let root = cwd;
356
366
  try {
@@ -370,6 +380,20 @@ function recapAtris(args = []) {
370
380
  const daysIdx = args.indexOf('--days');
371
381
  const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
372
382
  const data = buildRecapData(root, { days });
383
+ // Before any setup, or when the board has nothing to say, the useful
384
+ // first minute is what your ai agent already did in this folder today.
385
+ const boardQuiet = data.empty
386
+ || ['shipped', 'waiting', 'checking', 'inProgress'].every((k) => !(data[k] || []).length);
387
+ if (boardQuiet && !args.includes('--verbose') && !args.includes('--full') && !args.includes('--share')) {
388
+ const today = buildAgentToday({ cwd });
389
+ // A brand new folder already names its last commit in the first
390
+ // minute, so commits alone keep that; an ai session here wins.
391
+ const fresh = data.empty && isFreshWorkspace(root) && !inProjectSubdir;
392
+ if (hasActivity(today) && (ranAgent(today) || !fresh)) {
393
+ console.log(args.includes('--json') ? JSON.stringify(today, null, 2) : renderAgentToday(today));
394
+ return;
395
+ }
396
+ }
373
397
  if (
374
398
  data.empty
375
399
  && isFreshWorkspace(root)
@@ -110,10 +110,6 @@ function buildDoneTasks(root, cutoff) {
110
110
  });
111
111
  }
112
112
 
113
- function buildLandings(root, cutoff) {
114
- return buildDoneTasks(root, cutoff).filter(task => task.landed);
115
- }
116
-
117
113
  function landingStatusText(task) {
118
114
  const status = String(task?.landing_status || 'unchecked').replace(/_/g, ' ');
119
115
  const reason = String(task?.landing_reason || '').replace(/_/g, ' ');
@@ -9,7 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const { execSync, spawnSync } = require('child_process');
12
+ const { spawnSync } = require('child_process');
13
13
 
14
14
  function findReviewEngine() {
15
15
  // Look for review_engine.py in common locations
package/commands/setup.js CHANGED
@@ -1,5 +1,3 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
1
  const { loadCredentials, promptUser } = require('../utils/auth');
4
2
  const { apiRequestJson } = require('../utils/api');
5
3
 
@@ -73,6 +71,12 @@ async function setupAtris() {
73
71
  token: creds.token,
74
72
  });
75
73
 
74
+ if (!result.ok && result.status === 401) {
75
+ console.error('\n Your saved login has expired, so Atris could not list your businesses.');
76
+ console.error(' Run "atris login", then "atris setup" again.');
77
+ process.exit(1);
78
+ }
79
+
76
80
  if (!result.ok) {
77
81
  console.error(`\n Could not fetch businesses: ${result.error || 'Unknown error'}`);
78
82
  console.error(' You can add one later with: atris business add <slug>');