atris 3.57.0 → 3.57.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.
@@ -6,7 +6,7 @@
6
6
  * atris gmail read <id> [--account <id>] - Read specific email
7
7
  * atris gmail archive <id> [...] [--account <id>] - Archive messages
8
8
  * atris gmail triage [--account <id>] [--limit N] - Record keep or archive verdicts
9
- * atris gmail verdicts [--account <id>] [--limit N] - List recent Gmail verdicts
9
+ * atris gmail verdicts [--summary] [--account <id>] [--limit N] - List recent Gmail verdicts
10
10
  * atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - Send an email
11
11
  * atris gmail voice [account] [--clear] - Edit an account's writing voice
12
12
  * atris gmail connect [name] - Connect or reconnect a Gmail account
@@ -42,7 +42,7 @@ const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
42
42
  const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
43
43
  const GMAIL_VOICE_USAGE = 'usage: atris gmail voice [account] [--clear]';
44
44
  const GMAIL_TRIAGE_USAGE = 'usage: atris gmail triage [--account <id>] [--limit N]';
45
- const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--account <id>] [--limit N]';
45
+ const GMAIL_VERDICTS_USAGE = 'usage: atris gmail verdicts [--summary] [--account <id>] [--limit N]';
46
46
  const GMAIL_BULK_DOMAINS = [
47
47
  'campaign-archive.com',
48
48
  'constantcontact.com',
@@ -70,6 +70,7 @@ function appendGmailVerdicts(verdicts, options = {}) {
70
70
  message_id: String(row.message_id || ''),
71
71
  ...(row.from ? { from: row.from } : {}),
72
72
  ...(row.subject ? { subject: row.subject } : {}),
73
+ ...(typeof row.reason === 'string' && row.reason ? { reason: row.reason } : {}),
73
74
  }));
74
75
  fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8');
75
76
  }
@@ -84,24 +85,56 @@ function readGmailVerdicts(options = {}) {
84
85
  throw error;
85
86
  }
86
87
  const account = String(options.account || '').trim();
87
- const limit = Number.isInteger(options.limit) ? options.limit : 20;
88
- return lines.reduce((rows, line) => {
88
+ const limit = options.limit === null ? null : (Number.isInteger(options.limit) ? options.limit : 20);
89
+ const rows = lines.reduce((matches, line) => {
89
90
  try {
90
91
  const row = JSON.parse(line);
91
- if (!account || row.account === account) rows.push(row);
92
+ if (!account || row.account === account) matches.push(row);
92
93
  } catch {}
93
- return rows;
94
- }, []).reverse().slice(0, limit);
94
+ return matches;
95
+ }, []).reverse();
96
+ return limit === null ? rows : rows.slice(0, limit);
97
+ }
98
+
99
+ function summarizeGmailVerdicts(rows = []) {
100
+ const groups = new Map();
101
+ for (const row of rows) {
102
+ const date = String(row.ts || '').slice(0, 10);
103
+ const account = String(row.account || '');
104
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
105
+ const key = `${date}\n${account}`;
106
+ const summary = groups.get(key) || { date, account, keep: 0, archive: 0 };
107
+ summary[row.verdict === 'keep' ? 'keep' : 'archive'] += 1;
108
+ groups.set(key, summary);
109
+ }
110
+ return [...groups.values()].sort((left, right) => (
111
+ right.date.localeCompare(left.date) || left.account.localeCompare(right.account)
112
+ ));
95
113
  }
96
114
 
97
115
  function printGmailVerdicts(options = {}) {
98
- const rows = readGmailVerdicts(options);
116
+ const rows = readGmailVerdicts(options.summary ? { ...options, limit: null } : options);
117
+ if (options.summary) {
118
+ const summaries = summarizeGmailVerdicts(rows);
119
+ if (!summaries.length) {
120
+ console.log('no gmail verdicts found.');
121
+ return summaries;
122
+ }
123
+ for (const row of summaries) {
124
+ console.log(`${row.date} account ${row.account}, keep ${row.keep}, archive ${row.archive}`);
125
+ }
126
+ return summaries;
127
+ }
99
128
  if (!rows.length) {
100
129
  console.log('no gmail verdicts found.');
101
130
  return rows;
102
131
  }
103
132
  for (const row of rows) {
104
- const details = [row.from && `from ${row.from}`, row.subject && `subject ${row.subject}`].filter(Boolean);
133
+ const details = [
134
+ row.reason && `reason ${row.reason}`,
135
+ row.from && `from ${row.from}`,
136
+ row.subject && `subject ${row.subject}`,
137
+ ].filter(Boolean);
105
138
  console.log(`${row.ts} ${row.verdict || 'archive'} ${row.message_id} account ${row.account}${details.length ? `, ${details.join(', ')}` : ''}`);
106
139
  }
107
140
  return rows;
@@ -127,9 +160,14 @@ function parseGmailTriageArgs(args = []) {
127
160
  function parseGmailVerdictsArgs(args = []) {
128
161
  let account = null;
129
162
  let limit = 20;
163
+ let summary = false;
130
164
  for (let i = 0; i < args.length; i += 1) {
131
165
  const arg = args[i];
132
166
  const value = String(args[i + 1] || '').trim();
167
+ if (arg === '--summary') {
168
+ summary = true;
169
+ continue;
170
+ }
133
171
  if (arg === '--account' && value && !value.startsWith('--')) account = value;
134
172
  else if (arg === '--limit' && /^\d+$/.test(value) && Number(value) > 0) limit = Number(value);
135
173
  else {
@@ -138,7 +176,7 @@ function parseGmailVerdictsArgs(args = []) {
138
176
  }
139
177
  i += 1;
140
178
  }
141
- return { account, limit };
179
+ return { account, limit, summary };
142
180
  }
143
181
 
144
182
  function gmailAccountStatePath() {
@@ -378,25 +416,27 @@ function gmailMessageHeaderText(message = {}) {
378
416
  return lines.join('\n').toLowerCase();
379
417
  }
380
418
 
381
- function gmailTriageVerdict(message = {}) {
419
+ function gmailTriageVerdict(message = {}, options = {}) {
382
420
  const from = String(message.from || message.sender || '').toLowerCase();
421
+ let result;
383
422
  if (/(?:^|[^a-z0-9])(?:no[-_.]?reply|do[-_.]?not[-_.]?reply)(?:[^a-z0-9]|$)/i.test(from)) {
384
- return 'archive';
423
+ result = { verdict: 'archive', reason: 'noreply sender' };
385
424
  }
386
425
 
387
426
  const domain = from.match(/@([a-z0-9.-]+)/i)?.[1] || '';
388
- if (GMAIL_BULK_DOMAINS.some((bulkDomain) => domain === bulkDomain || domain.endsWith(`.${bulkDomain}`))) {
389
- return 'archive';
427
+ if (!result && GMAIL_BULK_DOMAINS.some((bulkDomain) => domain === bulkDomain || domain.endsWith(`.${bulkDomain}`))) {
428
+ result = { verdict: 'archive', reason: 'bulk mail domain' };
390
429
  }
391
430
 
392
431
  const headers = gmailMessageHeaderText(message);
393
- if (/(?:^|\n)(?:list-id|list-unsubscribe|mailing-list|x-campaign-id|x-mailing-list)\s*:/i.test(headers)
432
+ if (!result && (/(?:^|\n)(?:list-id|list-unsubscribe|mailing-list|x-campaign-id|x-mailing-list)\s*:/i.test(headers)
394
433
  || /(?:^|\n)precedence\s*:\s*(?:bulk|junk|list)/i.test(headers)
395
- || /\bunsubscribe\b/i.test(headers)) {
396
- return 'archive';
434
+ || /\bunsubscribe\b/i.test(headers))) {
435
+ result = { verdict: 'archive', reason: 'list or unsubscribe headers' };
397
436
  }
398
437
 
399
- return 'keep';
438
+ if (!result) result = { verdict: 'keep', reason: 'personal sender' };
439
+ return options.details ? result : result.verdict;
400
440
  }
401
441
 
402
442
  async function gmailTriage(options = {}) {
@@ -404,14 +444,18 @@ async function gmailTriage(options = {}) {
404
444
  const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 25;
405
445
  const messages = await gmailInbox({ accountId, limit, quiet: true });
406
446
  const ts = new Date().toISOString();
407
- const rows = messages.map((message) => ({
408
- ts,
409
- account: accountId,
410
- verdict: gmailTriageVerdict(message),
411
- message_id: String(message.id || message.message_id || ''),
412
- from: String(message.from || message.sender || 'Unknown'),
413
- subject: String(message.subject || '(no subject)'),
414
- }));
447
+ const rows = messages.map((message) => {
448
+ const { verdict, reason } = gmailTriageVerdict(message, { details: true });
449
+ return {
450
+ ts,
451
+ account: accountId,
452
+ verdict,
453
+ reason,
454
+ message_id: String(message.id || message.message_id || ''),
455
+ from: String(message.from || message.sender || 'Unknown'),
456
+ subject: String(message.subject || '(no subject)'),
457
+ };
458
+ });
415
459
  appendGmailVerdicts(rows, { root: options.root, filePath: options.filePath });
416
460
 
417
461
  const keepCount = rows.filter((row) => row.verdict === 'keep').length;
@@ -832,7 +876,7 @@ async function gmailCommand(subcommand, ...args) {
832
876
  console.log(' atris gmail read <id> [--account <id>] - read specific email');
833
877
  console.log(' atris gmail archive <id> [...] [--account <id>] - archive messages (reversible, all mail keeps them)');
834
878
  console.log(' atris gmail triage [--account <id>] [--limit N] - record keep or archive verdicts without changing mail');
835
- console.log(' atris gmail verdicts [--account <id>] [--limit N] - list recent gmail verdicts');
879
+ console.log(' atris gmail verdicts [--summary] [--account <id>] [--limit N] - list recent gmail verdicts');
836
880
  console.log(' atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>] - send an email');
837
881
  console.log(' atris gmail voice [account] [--clear] - edit or clear an account writing voice');
838
882
  console.log(' atris gmail connect [name] - connect or reconnect a gmail account');
@@ -9934,6 +9934,15 @@ async function executeMissionRunTicksPhase(context) {
9934
9934
  mission = resolveMission(mission.id, cwd) || mission;
9935
9935
  const remainingBudgetSeconds = missionFullBudgetRemainingSeconds(mission);
9936
9936
  const explicitExit = ['complete', 'stopped', 'paused'].includes(String(mission.status || ''));
9937
+ if (!pauseReason
9938
+ && detachedDriverLifecycle
9939
+ && mission.always_on
9940
+ && missionSpendsFullBudget(mission)
9941
+ && remainingBudgetSeconds <= 0
9942
+ && !explicitExit
9943
+ && !controller.signal.aborted) {
9944
+ pauseReason = 'budget-exhausted';
9945
+ }
9937
9946
  const healthyCycleBoundary = !pauseReason || pauseReason === 'max-ticks-reached';
9938
9947
  const keepDetachedFullBudgetDriverAlive = Boolean(
9939
9948
  detachedDriverLifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.57.0",
3
+ "version": "3.57.1",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "commands/",
15
15
  "decks/",
16
16
  "scripts/outbound-artifact-gate.js",
17
+ "scripts/member-operate.mjs",
17
18
  "scripts/agent_worktree.py",
18
19
  "scripts/det/",
19
20
  "utils/",
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // member-operate: the one executed slice of a member alive tick.
3
+ //
4
+ // Contract (lib/member-alive.js runMemberOperateScript):
5
+ // node scripts/member-operate.mjs <member> --json --max-wall <60..1800>
6
+ // --execute --confirm-autonomy-policy [--agent claude] [--model X] [--no-prime]
7
+ // stdout: final JSON only (last line wins). stderr: live progress, prefixed
8
+ // with ATRIS_MEMBER_OPERATE_PROGRESS\t so the alive loop can stream phases.
9
+ //
10
+ // This script is a dispatcher, not an engine. It delegates to
11
+ // `atris member run <member>`, which resumes the member's active mission or
12
+ // chooses one bounded useful task, and the mission itself carries its runner
13
+ // (codex_goal, claude, atris2, ...). Swapping engines is mission/member
14
+ // config, never an edit here.
15
+
16
+ import { spawn } from 'node:child_process';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import process from 'node:process';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const PROGRESS_PREFIX = 'ATRIS_MEMBER_OPERATE_PROGRESS\t';
23
+
24
+ function progress(payload) {
25
+ process.stderr.write(`${PROGRESS_PREFIX}${JSON.stringify(payload)}\n`);
26
+ }
27
+
28
+ function readFlag(args, name, fallback = '') {
29
+ const index = args.indexOf(name);
30
+ if (index === -1 || index === args.length - 1) return fallback;
31
+ return args[index + 1];
32
+ }
33
+
34
+ function finish(result) {
35
+ process.stdout.write(`${JSON.stringify(result)}\n`);
36
+ process.exit(result.ok === false ? 1 : 0);
37
+ }
38
+
39
+ function collectJsonSignals(text) {
40
+ // member run / mission run print JSON payloads along the way; harvest the
41
+ // fields the alive loop cares about without depending on exact shapes.
42
+ const signals = { receipt_path: null, needs_user: false, summary: null };
43
+ for (const line of String(text || '').split(/\r?\n/)) {
44
+ const trimmed = line.trim();
45
+ if (!trimmed.startsWith('{')) continue;
46
+ try {
47
+ const parsed = JSON.parse(trimmed);
48
+ if (parsed.receipt_path) signals.receipt_path = parsed.receipt_path;
49
+ if (parsed.mission?.receipt_path) signals.receipt_path = parsed.mission.receipt_path;
50
+ if (parsed.needs_user === true) signals.needs_user = true;
51
+ const landing = parsed.result?.landing || parsed.landing || null;
52
+ if (landing?.changed) signals.summary = landing.changed;
53
+ else if (parsed.summary) signals.summary = parsed.summary;
54
+ } catch {
55
+ // Partial or non-JSON line; keep streaming.
56
+ }
57
+ }
58
+ return signals;
59
+ }
60
+
61
+ const argv = process.argv.slice(2);
62
+ const member = argv[0] && !argv[0].startsWith('--') ? argv[0] : '';
63
+ const args = argv.slice(1);
64
+
65
+ if (!member) {
66
+ finish({ ok: false, reason: 'member_required', executed: false });
67
+ }
68
+
69
+ const execute = args.includes('--execute');
70
+ const confirmed = args.includes('--confirm-autonomy-policy');
71
+ if (!execute || !confirmed) {
72
+ finish({ ok: true, reason: 'operate_dry_run', member, executed: false });
73
+ }
74
+
75
+ const maxWall = Math.max(60, Math.min(1800, Number(readFlag(args, '--max-wall', '900')) || 900));
76
+ const agent = String(readFlag(args, '--agent', '')).trim().toLowerCase();
77
+ const model = String(readFlag(args, '--model', '')).trim();
78
+
79
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
80
+ const atrisBin = process.env.ATRIS_BIN || path.join(scriptDir, '..', 'bin', 'atris.js');
81
+ if (!fs.existsSync(atrisBin)) {
82
+ finish({ ok: false, reason: 'atris_bin_not_found', member, executed: false });
83
+ }
84
+
85
+ const runArgs = [atrisBin, 'member', 'run', member, '--json', '--max-wall', String(maxWall)];
86
+ if (agent === 'claude') runArgs.push('--runner', 'claude');
87
+ if (model) runArgs.push('--model', model);
88
+
89
+ progress({ kind: 'phase', text: `Dispatching one bounded ${member} slice (wall cap ${maxWall}s).` });
90
+
91
+ const child = spawn(process.execPath, runArgs, {
92
+ cwd: process.cwd(),
93
+ env: process.env,
94
+ stdio: ['ignore', 'pipe', 'pipe'],
95
+ });
96
+
97
+ let output = '';
98
+ const forward = (chunk) => {
99
+ const text = chunk.toString();
100
+ output += text;
101
+ // Keep stdout clean for the final JSON; the human-visible stream is stderr.
102
+ process.stderr.write(text);
103
+ };
104
+ child.stdout.on('data', forward);
105
+ child.stderr.on('data', forward);
106
+
107
+ const wallTimer = setTimeout(() => {
108
+ progress({ kind: 'phase', text: `Wall cap ${maxWall}s reached; stopping the slice.` });
109
+ child.kill('SIGTERM');
110
+ setTimeout(() => child.kill('SIGKILL'), 10000).unref();
111
+ }, maxWall * 1000);
112
+
113
+ child.on('close', (code) => {
114
+ clearTimeout(wallTimer);
115
+ const signals = collectJsonSignals(output);
116
+ const ok = code === 0;
117
+ finish({
118
+ ok,
119
+ reason: ok ? 'operate_complete' : 'operate_failed',
120
+ member,
121
+ executed: true,
122
+ exit_code: code,
123
+ max_wall_seconds: maxWall,
124
+ needs_user: signals.needs_user,
125
+ receipt_path: signals.receipt_path,
126
+ summary: signals.summary,
127
+ });
128
+ });
129
+
130
+ child.on('error', (error) => {
131
+ clearTimeout(wallTimer);
132
+ finish({ ok: false, reason: `operate_spawn_failed: ${error.message}`, member, executed: true });
133
+ });