atris 3.57.0 → 3.57.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/integrations.js +71 -27
- package/commands/mission.js +9 -0
- package/package.json +2 -1
- package/scripts/member-operate.mjs +151 -0
package/commands/integrations.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
92
|
+
if (!account || row.account === account) matches.push(row);
|
|
92
93
|
} catch {}
|
|
93
|
-
return
|
|
94
|
-
}, []).reverse()
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
+
|| /\bunsubscribe\b/i.test(headers))) {
|
|
435
|
+
result = { verdict: 'archive', reason: 'list or unsubscribe headers' };
|
|
397
436
|
}
|
|
398
437
|
|
|
399
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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');
|
package/commands/mission.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "3.57.2",
|
|
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,151 @@
|
|
|
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
|
+
// Resolution order: explicit env, repo checkout beside this script, then the
|
|
81
|
+
// installed CLI on PATH (cloud workspaces carry this script without the repo).
|
|
82
|
+
function resolveAtrisBin() {
|
|
83
|
+
const candidates = [
|
|
84
|
+
process.env.ATRIS_BIN,
|
|
85
|
+
path.join(scriptDir, '..', 'bin', 'atris.js'),
|
|
86
|
+
].filter(Boolean);
|
|
87
|
+
for (const candidate of candidates) {
|
|
88
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
89
|
+
}
|
|
90
|
+
const pathDirs = String(process.env.PATH || '').split(path.delimiter);
|
|
91
|
+
for (const dir of pathDirs) {
|
|
92
|
+
if (!dir) continue;
|
|
93
|
+
const candidate = path.join(dir, 'atris');
|
|
94
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const atrisBin = resolveAtrisBin();
|
|
99
|
+
if (!atrisBin) {
|
|
100
|
+
finish({ ok: false, reason: 'atris_bin_not_found', member, executed: false });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const runArgs = [atrisBin, 'member', 'run', member, '--json', '--max-wall', String(maxWall)];
|
|
104
|
+
if (agent === 'claude') runArgs.push('--runner', 'claude');
|
|
105
|
+
if (model) runArgs.push('--model', model);
|
|
106
|
+
|
|
107
|
+
progress({ kind: 'phase', text: `Dispatching one bounded ${member} slice (wall cap ${maxWall}s).` });
|
|
108
|
+
|
|
109
|
+
const child = spawn(process.execPath, runArgs, {
|
|
110
|
+
cwd: process.cwd(),
|
|
111
|
+
env: process.env,
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
let output = '';
|
|
116
|
+
const forward = (chunk) => {
|
|
117
|
+
const text = chunk.toString();
|
|
118
|
+
output += text;
|
|
119
|
+
// Keep stdout clean for the final JSON; the human-visible stream is stderr.
|
|
120
|
+
process.stderr.write(text);
|
|
121
|
+
};
|
|
122
|
+
child.stdout.on('data', forward);
|
|
123
|
+
child.stderr.on('data', forward);
|
|
124
|
+
|
|
125
|
+
const wallTimer = setTimeout(() => {
|
|
126
|
+
progress({ kind: 'phase', text: `Wall cap ${maxWall}s reached; stopping the slice.` });
|
|
127
|
+
child.kill('SIGTERM');
|
|
128
|
+
setTimeout(() => child.kill('SIGKILL'), 10000).unref();
|
|
129
|
+
}, maxWall * 1000);
|
|
130
|
+
|
|
131
|
+
child.on('close', (code) => {
|
|
132
|
+
clearTimeout(wallTimer);
|
|
133
|
+
const signals = collectJsonSignals(output);
|
|
134
|
+
const ok = code === 0;
|
|
135
|
+
finish({
|
|
136
|
+
ok,
|
|
137
|
+
reason: ok ? 'operate_complete' : 'operate_failed',
|
|
138
|
+
member,
|
|
139
|
+
executed: true,
|
|
140
|
+
exit_code: code,
|
|
141
|
+
max_wall_seconds: maxWall,
|
|
142
|
+
needs_user: signals.needs_user,
|
|
143
|
+
receipt_path: signals.receipt_path,
|
|
144
|
+
summary: signals.summary,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
child.on('error', (error) => {
|
|
149
|
+
clearTimeout(wallTimer);
|
|
150
|
+
finish({ ok: false, reason: `operate_spawn_failed: ${error.message}`, member, executed: true });
|
|
151
|
+
});
|