atris 3.74.9 → 3.74.10

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/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, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
1
+ const { AGENT_TOKEN_EXPIRED_DETAIL, assertSafeLoginWrite, 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');
@@ -939,6 +939,7 @@ function activeProfileName(credentials) {
939
939
  function writeGlobalCredentials(profile) {
940
940
  const kept = preserveAgentClientLogin();
941
941
  const credentialsPath = getCredentialsPath();
942
+ assertSafeLoginWrite(credentialsPath);
942
943
  fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
943
944
  try { fs.chmodSync(credentialsPath, 0o600); } catch {}
944
945
  return kept;
@@ -1167,7 +1168,7 @@ function resolveProfile() {
1167
1168
  const profile = loadProfile(p);
1168
1169
  return profile?.email?.toLowerCase().includes(q);
1169
1170
  });
1170
- if (match) {
1171
+ if (match && loadProfile(match)) {
1171
1172
  process.stdout.write(match);
1172
1173
  process.exit(0);
1173
1174
  }
@@ -1251,7 +1252,7 @@ function shellInit() {
1251
1252
  ' if [[ $? -eq 0 && "$_tty" == /dev/* ]]; then echo "$_tty"; return; fi',
1252
1253
  '}',
1253
1254
  'atris() {',
1254
- ' if [[ "$1" == "switch" && -n "$2" && "$2" != "--"* ]]; then',
1255
+ ' if [[ "$1" == "switch" && -n "$2" && "$2" != "-"* ]]; then',
1255
1256
  ' local _profile',
1256
1257
  ' _profile=$(command atris _resolve "$2" 2>/dev/null)',
1257
1258
  ' if [[ $? -eq 0 && -n "$_profile" ]]; then',
@@ -3547,25 +3547,27 @@ async function computerChat(token, ctx, initialOptions = {}) {
3547
3547
  }
3548
3548
 
3549
3549
  if (scriptedInput !== null) {
3550
- for (const rawLine of scriptedInput.split(/\r?\n/)) {
3550
+ const messages = oneShotMessage ? [scriptedInput] : scriptedInput.split(/\r?\n/);
3551
+ for (const rawLine of messages) {
3551
3552
  const line = String(rawLine || '').trim();
3552
3553
  if (!line) continue;
3553
- if (line === '/exit' || line === '/quit') break;
3554
- if (line.startsWith('/run ')) {
3554
+ const isCommand = line.startsWith('/') && !/[\r\n]/.test(line);
3555
+ if (isCommand && (line === '/exit' || line === '/quit')) break;
3556
+ if (isCommand && line.startsWith('/run ')) {
3555
3557
  await computerRun(token, line.slice(5), ctx);
3556
3558
  continue;
3557
3559
  }
3558
- if (line === '/pwd') {
3560
+ if (isCommand && line === '/pwd') {
3559
3561
  await computerRun(token, 'pwd', ctx);
3560
3562
  continue;
3561
3563
  }
3562
- if (line === '/audit' || line.startsWith('/audit ')) {
3564
+ if (isCommand && (line === '/audit' || line.startsWith('/audit '))) {
3563
3565
  const rawLimit = line.split(/\s+/, 2)[1];
3564
3566
  const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 10;
3565
3567
  await computerAudit(token, ctx, Number.isFinite(limit) ? limit : 10);
3566
3568
  continue;
3567
3569
  }
3568
- if (line.startsWith('/')) {
3570
+ if (isCommand) {
3569
3571
  const command = line.split(/\s+/, 1)[0];
3570
3572
  if (!KNOWN_CHAT_COMMANDS.has(command)) {
3571
3573
  console.log(`Unknown command: ${command}`);
@@ -3573,7 +3575,7 @@ async function computerChat(token, ctx, initialOptions = {}) {
3573
3575
  }
3574
3576
  continue;
3575
3577
  }
3576
- sessionId = await sendBusinessChat(token, ctx, line, sessionId, false, null, {
3578
+ sessionId = await sendBusinessChat(token, ctx, oneShotMessage ? rawLine : line, sessionId, false, null, {
3577
3579
  worker,
3578
3580
  model,
3579
3581
  systemPrompt: chatSystemPrompt,
@@ -204,7 +204,8 @@ async function businessDiff(slug) {
204
204
  const localContent = fs.readFileSync(localPath, 'utf8');
205
205
 
206
206
  if (f.isNew) {
207
- console.log(` + ${f.path.replace(/^\//, '')} (new file, ${localContent.split('\n').length} lines)`);
207
+ const lineCount = localContent.split('\n').length - (localContent.endsWith('\n') ? 1 : 0);
208
+ console.log(` + ${f.path.replace(/^\//, '')} (new file, ${lineCount} lines)`);
208
209
  } else {
209
210
  // Show a simple line-count diff
210
211
  console.log(` ~ ${f.path.replace(/^\//, '')} (modified)`);
@@ -1226,7 +1226,8 @@ async function calendarSearch(args = []) {
1226
1226
  }
1227
1227
 
1228
1228
  async function calendarToday() {
1229
- await calendarRange("Today's events", { days: 1 });
1229
+ const { start, end } = localDayBounds();
1230
+ await calendarRange("Today's events", { timeMin: start.toISOString(), timeMax: end.toISOString() });
1230
1231
  }
1231
1232
 
1232
1233
  async function calendarYesterday() {
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const os = require('os');
5
5
  const { execFileSync, spawnSync } = require('child_process');
6
- const { loadCredentials } = require('../utils/auth');
6
+ const { loadCredentials, ensureFreshCredentials } = require('../utils/auth');
7
7
  const { resolveBackendRoot } = require('../utils/backend-root');
8
8
  const { apiRequestJson } = require('../utils/api');
9
9
  const { runAliveTick } = require('../lib/member-alive');
@@ -4942,7 +4942,7 @@ async function memberPush(name) {
4942
4942
  process.exit(1);
4943
4943
  }
4944
4944
 
4945
- const creds = loadCredentials();
4945
+ const creds = await ensureFreshCredentials(apiRequestJson);
4946
4946
  if (!creds || !creds.token) {
4947
4947
  console.error('Not logged in. Run: atris login');
4948
4948
  process.exit(1);
@@ -5007,7 +5007,7 @@ async function memberPush(name) {
5007
5007
  // --- PULL subcommand ---
5008
5008
 
5009
5009
  async function memberPull(nameOrAgentId) {
5010
- const creds = loadCredentials();
5010
+ const creds = await ensureFreshCredentials(apiRequestJson);
5011
5011
  if (!creds || !creds.token) {
5012
5012
  console.error('Not logged in. Run: atris login');
5013
5013
  process.exit(1);
@@ -20,19 +20,26 @@ const {
20
20
  sessionStatus,
21
21
  sessionsToday,
22
22
  healthToday,
23
- healthSummary,
24
23
  } = require('../lib/ninety-nine');
25
24
 
26
25
  function usage() {
27
26
  return [
28
27
  'usage: atris 99 focus <25|50|90>',
29
28
  ' atris 99 meditate <10|20|30>',
29
+ ' atris 99 show the timer and today',
30
+ ' atris 99 <25|50|90> start focus',
31
+ ' atris 99 <10|20|30> start meditation',
32
+ ' atris 99 f <25|50|90>',
33
+ ' atris 99 m <10|20|30>',
34
+ ' atris 99 start <focus|meditate> <minutes>',
35
+ ' atris 99 again repeat the last finished session',
30
36
  ' atris 99 stop [note words]',
31
37
  ' atris 99 pause',
32
38
  ' atris 99 resume',
33
39
  ' atris 99 status [--json]',
34
40
  ' atris 99 today [--json]',
35
41
  ' atris 99 sync [--dry-run]',
42
+ ' atris 99 help | --help',
36
43
  '',
37
44
  'sync is off without a sync block in ~/.atris/99/config.json (ATRIS_99_CONFIG overrides it).',
38
45
  'personal: {"sync":{"target":"personal","app_slug":"project-99"}}',
@@ -152,6 +159,23 @@ function appendRecord(logPath, previousText, record) {
152
159
  return previousText + line;
153
160
  }
154
161
 
162
+ function todayLine(result) {
163
+ return `Today: ${result.counts.focus} focus, ${result.counts.meditate} meditate, ${result.finished_minutes} min`;
164
+ }
165
+
166
+ function kindName(kind) {
167
+ return kind === 'focus' ? 'Focus' : 'Meditate';
168
+ }
169
+
170
+ function timeLeft(session, time) {
171
+ const seconds = sessionStatus(session, time).running.remaining_seconds;
172
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
173
+ }
174
+
175
+ function localEndTime(end) {
176
+ return end.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
177
+ }
178
+
155
179
  function ninetyNineAtris(args = process.argv.slice(3), {
156
180
  log = console.log,
157
181
  now = () => new Date(),
@@ -161,32 +185,48 @@ function ninetyNineAtris(args = process.argv.slice(3), {
161
185
  request,
162
186
  credentials,
163
187
  } = {}) {
164
- const [action, ...rest] = args;
188
+ let [action, ...rest] = args;
189
+ const overview = args.length === 0;
165
190
  // Words after stop belong to its note, including the word "help".
166
191
  if ((action !== 'stop' && argsWantHelp(args)) || (action === 'stop' && ['--help', '-h'].includes(rest[0]))) {
167
192
  log(usage());
168
193
  return 0;
169
194
  }
170
195
  if (action === 'sync') return syncNinetyNine(rest, { log, logPath, configPath, statePath, request, credentials });
196
+ if (action === 'start' && ['focus', 'meditate', 'f', 'm'].includes(rest[0])) [action, ...rest] = rest;
197
+ if (action === 'f') action = 'focus';
198
+ if (action === 'm') action = 'meditate';
199
+ for (const kind of ['focus', 'meditate']) {
200
+ if (validLength(kind, Number(action)) && String(Number(action)) === action) {
201
+ rest.unshift(action);
202
+ action = kind;
203
+ break;
204
+ }
205
+ }
171
206
  const json = rest.includes('--json');
172
207
  if (action === 'focus' || action === 'meditate') {
208
+ const lengths = action === 'focus' ? '25 | 50 | 90' : '10 | 20 | 30';
209
+ if (rest.length === 0) {
210
+ log(`Pick a length: atris 99 ${action} ${lengths}`);
211
+ return 1;
212
+ }
173
213
  if (rest.length !== 1 || !validLength(action, Number(rest[0])) || String(Number(rest[0])) !== rest[0]) {
174
- log(action === 'focus' ? 'choose focus 25, 50, or 90 minutes' : 'choose meditate 10, 20, or 30 minutes');
175
- return 2;
214
+ log(`Allowed ${action} lengths: ${lengths.replaceAll(' | ', ', ')} minutes.`);
215
+ return 1;
176
216
  }
177
217
  } else if (action === 'status' || action === 'today') {
178
218
  if (rest.length > 1 || (rest.length === 1 && !json)) {
179
219
  log(`usage: atris 99 ${action} [--json]`);
180
220
  return 2;
181
221
  }
182
- } else if (action === 'pause' || action === 'resume') {
222
+ } else if (action === 'pause' || action === 'resume' || action === 'again') {
183
223
  if (rest.length) {
184
224
  log(`usage: atris 99 ${action}`);
185
225
  return 2;
186
226
  }
187
- } else if (action !== 'stop') {
188
- log(usage());
189
- return 2;
227
+ } else if (action !== 'stop' && !overview) {
228
+ log(`Unknown: ${action}. Try: atris 99 help`);
229
+ return 1;
190
230
  }
191
231
 
192
232
  let lock;
@@ -222,55 +262,75 @@ function ninetyNineAtris(args = process.argv.slice(3), {
222
262
 
223
263
  if (action === 'stop') {
224
264
  if (!stopTarget) {
225
- log('nothing running');
265
+ log('Nothing running. Start one: atris 99 50');
226
266
  return 1;
227
267
  }
228
268
  const end = endRecord(stopTarget, time, rest.join(' '));
229
269
  appendRecord(file, text, end);
230
- log(`${stopTarget.kind} ${end.status}: ${end.status === 'done' ? stopTarget.planned_min : 0} finished minutes`);
270
+ records.push(end);
271
+ log(end.status === 'done'
272
+ ? `${kindName(stopTarget.kind)} ${stopTarget.planned_min} done.`
273
+ : "Stopped early, so it doesn't count.");
274
+ log(todayLine(sessionsToday(foldSessions(records), time)));
231
275
  return 0;
232
276
  }
233
277
 
234
278
  sessions = foldSessions(records);
235
279
  active = runningSession(sessions, time);
236
- if (action === 'focus' || action === 'meditate') {
280
+ if (action === 'again') {
281
+ const previous = sessions.filter((session) => session.status === 'done')
282
+ .sort((a, b) => Date.parse(b.ended_at) - Date.parse(a.ended_at))[0];
283
+ if (!previous) {
284
+ log('No finished session yet. Try: atris 99 50');
285
+ return 1;
286
+ }
287
+ action = previous.kind;
288
+ rest = [String(previous.planned_min)];
289
+ }
290
+ if (overview) {
291
+ if (!active) log('Nothing running.');
292
+ else log(`${kindName(active.kind)} · ${timeLeft(active, time)} left${active.status === 'paused' ? ' · paused' : ''}`);
293
+ log(todayLine(sessionsToday(sessions, time)));
294
+ log('Start: atris 99 50 Again: atris 99 again Help: atris 99 help');
295
+ } else if (action === 'focus' || action === 'meditate') {
237
296
  if (active) {
238
297
  log('a session is already running; use atris 99 stop first');
239
298
  return 1;
240
299
  }
241
300
  appendRecord(file, text, startRecord(action, Number(rest[0]), time, randomUUID()));
242
- log(`${action} started for ${rest[0]} minutes`);
301
+ const end = new Date(time.getTime() + Number(rest[0]) * 60000);
302
+ log(`${kindName(action)} ${rest[0]} started. Ends at ${localEndTime(end)}.`);
243
303
  } else if (action === 'pause' || action === 'resume') {
244
- let record;
245
- try {
246
- record = action === 'pause' ? pauseRecord(active, time) : resumeRecord(active, time);
247
- } catch (error) {
248
- log(error.message);
304
+ if (!active) {
305
+ log('Nothing running. Start one: atris 99 50');
306
+ return 1;
307
+ }
308
+ if (action === 'pause' && active.status === 'paused') {
309
+ log('Already paused. Resume: atris 99 resume');
310
+ return 1;
311
+ }
312
+ if (action === 'resume' && active.status !== 'paused') {
313
+ log('Not paused.');
249
314
  return 1;
250
315
  }
316
+ const record = action === 'pause' ? pauseRecord(active, time) : resumeRecord(active, time);
251
317
  appendRecord(file, text, record);
252
- log(`${active.kind} ${action === 'pause' ? 'paused' : 'resumed'}`);
318
+ if (action === 'pause') log(`Paused. ${timeLeft(active, time)} left.`);
319
+ else {
320
+ const end = new Date(Date.parse(active.planned_end_at) + time.getTime() - Date.parse(active.paused_at));
321
+ log(`Resumed. Ends at ${localEndTime(end)}.`);
322
+ }
253
323
  } else if (action === 'status') {
254
324
  const result = sessionStatus(active, time);
255
325
  if (json) log(JSON.stringify(result));
256
326
  else if (!active) log('nothing running');
257
- else {
258
- const seconds = result.running.remaining_seconds;
259
- log(`${active.kind}: ${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')} left${active.status === 'paused' ? ' (paused)' : ''}`);
260
- }
327
+ else log(`${kindName(active.kind)} · ${timeLeft(active, time)} left${active.status === 'paused' ? ' · paused' : ''}`);
261
328
  } else {
262
329
  const result = sessionsToday(sessions, time);
263
330
  const health = healthToday(records, time);
264
331
  if (health) result.health_day = health;
265
332
  if (json) log(JSON.stringify(result));
266
- else {
267
- for (const session of result.sessions) {
268
- log(`${session.kind} ${session.planned_min} minutes: ${session.status}`);
269
- }
270
- log(`${result.finished_minutes} finished minutes today`);
271
- log(`Today: ${result.counts.focus} focus, ${result.counts.meditate} meditate, ${result.finished_minutes} min`);
272
- if (health) log(healthSummary(health));
273
- }
333
+ else log(todayLine(result));
274
334
  }
275
335
  return 0;
276
336
  } catch (error) {
package/commands/orb.js CHANGED
@@ -153,7 +153,7 @@ function unknownOrbFlags(args = []) {
153
153
  function tailLines(text, count = 60) {
154
154
  const lines = String(text || '').split(/\r?\n/);
155
155
  if (lines[lines.length - 1] === '') lines.pop();
156
- return lines.slice(-count).join('\n');
156
+ return lines.slice(Math.max(0, lines.length - count)).join('\n');
157
157
  }
158
158
 
159
159
  async function runOrb(args = []) {
package/commands/pull.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
3
+ const { ensureFreshCredentials, abortOnAuthFailure } = require('../utils/auth');
4
4
  const { apiRequestJson } = require('../utils/api');
5
5
  const { findAllMembers } = require('./member');
6
6
  const { loadConfig } = require('../utils/config');
@@ -158,7 +158,7 @@ async function pullAtris() {
158
158
  process.exit(1);
159
159
  }
160
160
 
161
- const creds = loadCredentials();
161
+ const creds = await ensureFreshCredentials(apiRequestJson);
162
162
  if (!creds || !creds.token) {
163
163
  console.error('Not logged in. Run: atris login');
164
164
  process.exit(1);
@@ -206,7 +206,7 @@ async function pullAtris() {
206
206
 
207
207
  async function pullBusiness(slug) {
208
208
  const elapsedMs = startTimer();
209
- const creds = loadCredentials();
209
+ const creds = await ensureFreshCredentials(apiRequestJson);
210
210
  if (!creds || !creds.token) {
211
211
  console.error('Not logged in. Run: atris login');
212
212
  process.exit(1);
package/commands/push.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const readline = require('readline');
4
- const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
4
+ const { ensureFreshCredentials, abortOnAuthFailure } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
6
  const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
7
7
  const { loadManifest, saveManifest, buildManifest, computeLocalHashes, isIgnoredSyncPath, filterSyncFiles } = require('../lib/manifest');
@@ -457,7 +457,7 @@ async function pushAtris() {
457
457
  const timeoutSec = parsePushTimeoutSec(process.argv);
458
458
  let onlyPrefixes = null;
459
459
 
460
- const creds = loadCredentials();
460
+ const creds = await ensureFreshCredentials(apiRequestJson);
461
461
  if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
462
462
 
463
463
  // Determine source directory
package/commands/skill.js CHANGED
@@ -549,6 +549,11 @@ function skillFix(name) {
549
549
  const skillsDir = path.join(process.cwd(), 'atris', 'skills');
550
550
  const allSkills = findAllSkills(skillsDir);
551
551
 
552
+ if (allSkills.length === 0) {
553
+ console.error('No skills found in atris/skills/.');
554
+ process.exit(1);
555
+ }
556
+
552
557
  const targets = name === '--all'
553
558
  ? allSkills
554
559
  : allSkills.filter(s => s.folder === name || s.leafFolder === name);
package/commands/soul.js CHANGED
@@ -28,7 +28,8 @@ function readFile(filePath) {
28
28
  }
29
29
 
30
30
  function countFiles(dir) {
31
- try { return fs.readdirSync(dir, { recursive: true }).filter(f => !f.startsWith('.')).length; } catch { return 0; }
31
+ const isFile = (f) => { try { return fs.statSync(path.join(dir, f)).isFile(); } catch { return false; } }; // a dead link skips itself, not the count
32
+ try { return fs.readdirSync(dir, { recursive: true }).filter(f => !f.startsWith('.') && isFile(f)).length; } catch { return 0; }
32
33
  }
33
34
 
34
35
  // ── Soul snapshot ──────────────────────────────────────
@@ -268,7 +268,8 @@ function makeEvent({ ts, agent, event, summary, raw, source, root }) {
268
268
  const ms = timestampMs(ts);
269
269
  if (!ms) return null;
270
270
  const cleanAgent = sanitizeAgent(agent);
271
- const cleanSummary = sanitizeSummary(summary, cleanAgent);
271
+ // Snapshot headings are complete lines, not agent-prefixed activity.
272
+ const cleanSummary = sanitizeSummary(summary, event === 'snapshot' ? '' : cleanAgent);
272
273
  return {
273
274
  ts: new Date(ms).toISOString(),
274
275
  ms,
@@ -755,7 +756,7 @@ function renderJsonLine(record) {
755
756
  ts: record.ts,
756
757
  agent: sanitizeAgent(record.agent),
757
758
  event: record.event,
758
- summary: sanitizeSummary(record.summary, record.agent),
759
+ summary: sanitizeSummary(record.summary, record.event === 'snapshot' ? '' : record.agent),
759
760
  raw: record.raw || null,
760
761
  });
761
762
  }
@@ -292,7 +292,7 @@ function stringsCommand(argv) {
292
292
  Pairs with 'atris slop' (how copy reads), strings governs what words you ship.
293
293
  The registry lives in .atris/strings.json. Wire 'check --staged' into the pre-commit gate.
294
294
  `);
295
- return 0;
295
+ return !sub || ['help', '--help', '-h'].includes(sub) ? 0 : 2;
296
296
  }
297
297
 
298
298
  module.exports = {
@@ -377,7 +377,7 @@ function normalizeQuestions(payload, problem) {
377
377
  }
378
378
 
379
379
  function fallbackQuestions(problem) {
380
- const subject = (clipLine(problem, 90) || 'this problem').replace(/[?!.]+$/, '');
380
+ const subject = clipLine((String(problem || '').trim() || 'this problem').replace(/[?!.]+$/, ''), 90);
381
381
  return [
382
382
  {
383
383
  question: `What existing code path already owns "${subject}"?`,
@@ -1902,7 +1902,7 @@ async function executeAgentSDKFast(userInput) {
1902
1902
  } catch (error) {
1903
1903
  console.error(`✗ Error: ${error.message}`);
1904
1904
  console.log('');
1905
- console.log('💡 Hosted turns use https://api.atris.ai. For local development, set ATRIS_API_BASE to your own backend URL.');
1905
+ console.log('💡 start your local backend at http://127.0.0.1:8000.');
1906
1906
  process.exit(1);
1907
1907
  }
1908
1908
  }
package/commands/zen.js CHANGED
@@ -58,14 +58,15 @@ function makeOut(calm) {
58
58
  }
59
59
 
60
60
  // Name the three biggest places so a large count reads as "mostly agent
61
- // notes in atris/" rather than an unexplained total.
61
+ // notes in atris/" rather than an unexplained total. A lone fourth place is
62
+ // named too, since "and 1 more place" takes as much room and says less.
62
63
  function teamLine(n, places = []) {
63
64
  if (!n) return 'no unsaved team notes.';
64
65
  const head = n === 1 ? '1 unsaved team note left in place' : `${n} unsaved team notes left in place`;
65
66
  if (!places.length) return `${head}.`;
66
- const named = places.slice(0, 3).map((p) => `${p.place} ${p.count}`);
67
+ const named = places.slice(0, places.length === 4 ? 4 : 3).map((p) => `${p.place} ${p.count}`);
67
68
  const rest = places.length - named.length;
68
- if (rest) named.push(`and ${rest} more ${rest === 1 ? 'place' : 'places'}`);
69
+ if (rest) named.push(`and ${rest} more places`);
69
70
  return `${head}: ${named.join(', ')}.`;
70
71
  }
71
72
 
@@ -1099,7 +1099,7 @@ function sniffedProtectedLane(task) {
1099
1099
  function deniedTagOf(task) {
1100
1100
  const tag = String((task && task.tag) || '').trim().toLowerCase();
1101
1101
  return [...DENIED_TAGS].find((d) =>
1102
- tag === d || tag.split(/[^a-z0-9]+/).some((w) => w === d || w.replace(/s$/, '') === d)) || null;
1102
+ tag === d || tag.split(/[^a-z0-9]+/).some((w) => w === d || w.replace(/s$/, '') === d || w === d.replace(/s$/, ''))) || null;
1103
1103
  }
1104
1104
 
1105
1105
  function protectedLaneOf(task) {
@@ -1197,8 +1197,7 @@ function evaluateAutoAccept(task, options = {}) {
1197
1197
  // exact-match cousins: match denied lanes on whole words with a plural
1198
1198
  // strip, so a tag variant never slips money/deploy work past the human.
1199
1199
  const tag = String(task.tag || '').trim().toLowerCase();
1200
- const deniedTag = [...DENIED_TAGS].find((d) =>
1201
- tag === d || tag.split(/[^a-z0-9]+/).some((w) => w === d || w.replace(/s$/, '') === d));
1200
+ const deniedTag = deniedTagOf(task);
1202
1201
  if (deniedTag) return { eligible: false, ref, reason: `denied_tag_${deniedTag}` };
1203
1202
 
1204
1203
  // A protected lane declared in prose is not a protection, it is a note.
package/lib/autoland.js CHANGED
@@ -187,13 +187,14 @@ function cronMarker(root) {
187
187
  }
188
188
 
189
189
  function buildCronLine(root) {
190
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
190
191
  const node = process.execPath;
191
192
  const bin = path.join(root, 'bin', 'atris.js');
192
193
  const cliBin = fs.existsSync(bin) ? bin : 'atris';
193
194
  const logDir = path.join(stateHome(), '.atris', 'overnight');
194
195
  const logFile = path.join(logDir, `autoland-${path.basename(root)}.log`);
195
- const invoke = cliBin.endsWith('.js') ? `${node} ${cliBin}` : cliBin;
196
- return `14 * * * * cd ${root} && ${invoke} autoland tick >> ${logFile} 2>&1 # ${cronMarker(root)}`;
196
+ const invoke = cliBin.endsWith('.js') ? `${quote(node)} ${quote(cliBin)}` : cliBin;
197
+ return `14 * * * * cd ${quote(root)} && ${invoke} autoland tick >> ${quote(logFile)} 2>&1 # ${cronMarker(root)}`;
197
198
  }
198
199
 
199
200
  function readCrontab() {
@@ -499,7 +500,7 @@ function dejargon(line, options = {}) {
499
500
  .replace(/--([a-z][a-z-]*)/g, (_match, flag) => flag.replace(/-/g, ' '))
500
501
  .replace(/\b[a-z0-9]+(?:_[a-z0-9]+)+\b/gi, match => match.replace(/_/g, ' '))
501
502
  .replace(/\b[a-z]+(?:[A-Z][A-Za-z0-9]*)+\b/g, match => match.replace(/([a-z0-9])([A-Z])/g, '$1 $2'));
502
- if (options.preserveTickets !== true) value = value.replace(/\bCLI-\d+\b/g, '');
503
+ if (options.preserveTickets !== true) value = value.replace(/\(\s*CLI-\d+\s*\)|\bCLI-\d+\b/g, '');
503
504
  return value.replace(/\s+/g, ' ').trim();
504
505
  }
505
506
 
@@ -50,6 +50,14 @@ function readBrief(root, campaignId) {
50
50
  }
51
51
  }
52
52
 
53
+ function objectiveWithBrief(objective, brief) {
54
+ if (!brief) return objective;
55
+ // Mission objectives also become titles and log lines, so keep them on one line.
56
+ const line = objective.replace(/\s+/g, ' ').trim();
57
+ const briefLine = brief.replace(/\s+/g, ' ').trim();
58
+ return line.includes(briefLine) ? line : `${line} ${briefLine}`;
59
+ }
60
+
53
61
  function rankCampaignMembers(view, rsiSummary = null, events = null) {
54
62
  const seats = Array.isArray(view && view.seats) ? view.seats.map((s) => s.member).filter(Boolean) : [];
55
63
  const candidates = [...new Set([view && view.owner, ...seats].filter(Boolean))];
@@ -180,7 +188,7 @@ async function askOwner(root, view, prompt) {
180
188
 
181
189
  // Run one atris command and resolve with its exit code and output.
182
190
  // A live step an engine prints while a mission runs: "<member> <hh:mm> <what it is doing>".
183
- const ACTIVITY_LINE = /^\s*[a-z0-9][a-z0-9-]*\s+\d{1,2}:\d{2}\s{2,}\S/i;
191
+ const ACTIVITY_LINE = /^\s*[a-z0-9][a-z0-9._-]*\s+\d{1,2}:\d{2}\s{2,}\S/i;
184
192
 
185
193
  function runCli(args, { cwd, env = process.env, onLine = null } = {}) {
186
194
  return new Promise((resolve) => {
@@ -258,7 +266,7 @@ function makeLoop(root, options = {}, deps = {}) {
258
266
  proposals.forEach((proposal, index) => {
259
267
  const missionId = proposalId(proposal.objective);
260
268
  const key = claimKey(proposal.objective);
261
- if (brief && !proposal.objective.includes(brief)) proposal.objective = `${proposal.objective}\n\n${brief}`;
269
+ proposal.objective = objectiveWithBrief(proposal.objective, brief);
262
270
  append(id, 'mission_proposed', { mission_id: missionId, member: proposal.member, objective: proposal.objective, verify: proposal.verify, estimate_usd: proposal.estimate_usd || undefined }, proposal.member);
263
271
  append(id, 'mission_claimed', { mission_id: missionId, claim_key: key }, proposal.member);
264
272
  if (index === 0) append(id, 'step', { mission_id: missionId, ...plan }, current.owner);
@@ -282,7 +290,7 @@ function makeLoop(root, options = {}, deps = {}) {
282
290
  const current = view(id);
283
291
  const reserve = work.estimate_usd || defaultReserve(current);
284
292
  const brief = (deps.brief ? deps.brief(id) : readBrief(root, id)).trim();
285
- const objective = brief && !work.objective.includes(brief) ? `${work.objective}\n\n${brief}` : work.objective;
293
+ const objective = objectiveWithBrief(work.objective, brief);
286
294
  const started = await cli([
287
295
  'mission', 'start', objective, '--mission-id', work.mission_id, '--owner', work.member,
288
296
  '--runner', runner, '--verify', work.verify || current.judge, '--campaign', id, '--reserve', String(reserve),
@@ -299,7 +307,7 @@ function makeLoop(root, options = {}, deps = {}) {
299
307
  append(id, 'mission_vetoed', { mission_id: work.mission_id, reason: why.slice(0, 200) }, current.owner);
300
308
  return { mission_id: work.mission_id, started: false };
301
309
  }
302
- log(` ${work.mission_id} started for ${work.member}: ${work.objective}`);
310
+ log(` ${work.mission_id} started for ${work.member}: ${objective}`);
303
311
  // The engine's live steps ("researcher 06:47 editing stable.mjs") pass through to
304
312
  // the campaign's own output, so a screen watching the run can show the work.
305
313
  const stepsBefore = (view(id).missions.find((m) => m.id === work.mission_id) || {}).steps || 0;
package/lib/card.js CHANGED
@@ -114,7 +114,7 @@ function buildCard(spec = {}, opts = {}) {
114
114
  </style></head>
115
115
  <body><div class="card">${main}${foot}</div></body></html>`;
116
116
 
117
- return { html, width: size.w, height: size.h, kind, theme: spec.theme || 'atris', size: spec.size || 'og' };
117
+ return { html, width: size.w, height: size.h, kind, theme: themes[spec.theme] ? spec.theme : 'atris', size: spec.size || 'og' };
118
118
  }
119
119
 
120
120
  module.exports = { buildCard, SIZES, KINDS };
@@ -310,7 +310,7 @@ function scanChatLogs(options = {}) {
310
310
  scope: options.global === true ? 'global' : 'workspace',
311
311
  window_hours: maxAgeHours,
312
312
  sources: {
313
- ax_dirs: axLogDirs(cwd),
313
+ ax_dirs: axLogDirs(cwd, { global: options.global === true }),
314
314
  cursor_roots: cursorTranscriptRoots(cwd),
315
315
  },
316
316
  summary: {
@@ -1058,6 +1058,7 @@ function speakFirstMinute({
1058
1058
  log(JSON.stringify(freshMinuteJson(folderName(root), visible, { root }), null, 2));
1059
1059
  return 2;
1060
1060
  }
1061
+ if (asJson) return speakKeepWorkingMinute({ root, asJson, log });
1061
1062
  const screen = buildFirstMinute({
1062
1063
  root,
1063
1064
  fresh: isFresh,