atris 3.64.0 → 3.65.0
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/README.md +4 -0
- package/bin/atris.js +17 -0
- package/commands/agent-spawn.js +1 -0
- package/commands/engine.js +181 -63
- package/commands/hire.js +189 -0
- package/commands/mail.js +163 -0
- package/commands/mission.js +3 -2
- package/commands/stream.js +22 -1
- package/commands/team.js +355 -67
- package/lib/auto-accept-certified.js +83 -9
- package/lib/business-target.js +131 -0
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +16 -3
- package/lib/known-commands.js +1 -1
- package/lib/member-identity.js +56 -0
- package/lib/roster-models.js +11 -0
- package/lib/roster-stale.js +241 -0
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- package/package.json +1 -1
package/commands/mail.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris mail inboxes | read <inbox> | send <inbox> --to --subject --body
|
|
4
|
+
// Your agents' own email inboxes (the @atrismail.com ones), not your Gmail.
|
|
5
|
+
// For Gmail use `atris gmail`. Backend: /api/business/{id}/mail/...
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const { apiRequestJson } = require('../utils/api');
|
|
9
|
+
const { NOT_LOGGED_IN } = require('../lib/developer-api');
|
|
10
|
+
const {
|
|
11
|
+
parseArgs, businessOrSay, loginToken, errorText, clip,
|
|
12
|
+
} = require('../lib/business-target');
|
|
13
|
+
|
|
14
|
+
// The only flags each subcommand takes (plus --help, --json, --business).
|
|
15
|
+
const MAIL_FLAGS = {
|
|
16
|
+
inboxes: {},
|
|
17
|
+
read: { values: ['limit'] },
|
|
18
|
+
send: { switches: ['yes', 'dry_run'], values: ['to', 'subject', 'body'] },
|
|
19
|
+
};
|
|
20
|
+
const ALL_MAIL_FLAGS = { switches: ['yes', 'dry_run'], values: ['limit', 'to', 'subject', 'body'] };
|
|
21
|
+
|
|
22
|
+
function mailHelp(log = console.log) {
|
|
23
|
+
log(`usage: atris mail inboxes [--business <slug>] [--json]
|
|
24
|
+
atris mail read <inbox> [--limit 10] [--json]
|
|
25
|
+
atris mail send <inbox> --to <address> --subject "<subject>" --body "<text>" [--yes] [--dry-run]
|
|
26
|
+
|
|
27
|
+
your agents' own email inboxes (not your Gmail; for that use atris gmail).
|
|
28
|
+
<inbox> is the address, the part before the @, or the inbox id.
|
|
29
|
+
|
|
30
|
+
inboxes list every agent inbox in this business
|
|
31
|
+
read newest messages first (--limit 1 to 200, default 10)
|
|
32
|
+
send send one email from an agent inbox. asks before sending unless --yes.
|
|
33
|
+
--dry-run prints the email and sends nothing.
|
|
34
|
+
unknown flags are refused, so a typo never sends anything.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toList(value) {
|
|
38
|
+
if (Array.isArray(value)) return value.map(String);
|
|
39
|
+
if (typeof value === 'string' && value.trim().startsWith('[')) {
|
|
40
|
+
try { return JSON.parse(value).map(String); } catch { /* fall through */ }
|
|
41
|
+
}
|
|
42
|
+
return value ? [String(value)] : [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderInboxes(rows = []) {
|
|
46
|
+
if (!rows.length) return 'No agent inboxes yet. Hiring an agent makes one: atris hire "<name>" --job "<idea>"';
|
|
47
|
+
return rows.map((r) => `${r.email_address}${r.display_name ? ` (${r.display_name})` : ''}`).join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderMessages(inbox, rows = []) {
|
|
51
|
+
if (!rows.length) return `No mail in ${inbox.email_address} yet.`;
|
|
52
|
+
const lines = [];
|
|
53
|
+
for (const m of rows) {
|
|
54
|
+
const when = String(m.created_at || '').slice(0, 16).replace('T', ' ');
|
|
55
|
+
const out = m.direction === 'outbound';
|
|
56
|
+
const who = out ? `to ${toList(m.to_addresses).join(', ')}` : `from ${m.from_address || '?'}`;
|
|
57
|
+
lines.push(`${when} ${out ? 'sent' : 'got '} ${who}`);
|
|
58
|
+
lines.push(` ${clip(m.subject || '(no subject)', 100)}`);
|
|
59
|
+
const body = clip(m.body_text || '', 160);
|
|
60
|
+
if (body) lines.push(` ${body}`);
|
|
61
|
+
}
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// address, local part, or id -> the inbox row
|
|
66
|
+
function findInbox(rows, wanted) {
|
|
67
|
+
const w = String(wanted || '').trim().toLowerCase();
|
|
68
|
+
if (!w) return null;
|
|
69
|
+
return rows.find((r) => String(r.id).toLowerCase() === w)
|
|
70
|
+
|| rows.find((r) => String(r.email_address || '').toLowerCase() === w)
|
|
71
|
+
|| rows.find((r) => String(r.email_address || '').toLowerCase().split('@')[0] === w)
|
|
72
|
+
|| null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function askYesNo(question) {
|
|
76
|
+
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
79
|
+
rl.question(question, (answer) => {
|
|
80
|
+
rl.close();
|
|
81
|
+
resolve(/^y(es)?$/i.test(String(answer || '').trim()));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function mailCommand(args = [], deps = {}) {
|
|
87
|
+
const out = { log: deps.log || console.log, err: deps.err || console.error };
|
|
88
|
+
const { flags, pos } = parseArgs(args, ALL_MAIL_FLAGS);
|
|
89
|
+
const sub = pos[0];
|
|
90
|
+
if (flags.help || !sub || sub === 'help') { mailHelp(out.log); return sub || flags.help ? 0 : 1; }
|
|
91
|
+
if (!MAIL_FLAGS[sub]) {
|
|
92
|
+
out.err(`unknown mail command: ${sub}. try: atris mail inboxes | read | send`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
// Checked against this subcommand's own flags, before any request: an
|
|
96
|
+
// unknown or mistyped flag on send must never turn into a real email.
|
|
97
|
+
const { error } = parseArgs(args, MAIL_FLAGS[sub]);
|
|
98
|
+
if (error) { out.err(`${error}. see: atris mail --help`); return 1; }
|
|
99
|
+
const words = sub === 'inboxes' ? 1 : 2;
|
|
100
|
+
if (pos.length > words) {
|
|
101
|
+
out.err(`atris mail ${sub} got an extra word: "${pos[words]}". put text in quotes, like --body "hi there"`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const business = businessOrSay(flags, deps, out.err);
|
|
106
|
+
if (!business) return 1;
|
|
107
|
+
const token = await loginToken(deps);
|
|
108
|
+
if (!token) { out.err(NOT_LOGGED_IN); return 1; }
|
|
109
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
110
|
+
const base = `/business/${encodeURIComponent(business.id)}/mail`;
|
|
111
|
+
|
|
112
|
+
const listed = await request(`${base}/inboxes`, { method: 'GET', token });
|
|
113
|
+
if (!listed || !listed.ok) { out.err(`Could not load inboxes: ${errorText(listed)}`); return 1; }
|
|
114
|
+
const inboxes = Array.isArray(listed.data) ? listed.data : [];
|
|
115
|
+
|
|
116
|
+
if (sub === 'inboxes') {
|
|
117
|
+
out.log(flags.json ? JSON.stringify(inboxes) : renderInboxes(inboxes));
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!pos[1]) { out.err(`which inbox? atris mail ${sub} <inbox>`); return 1; }
|
|
122
|
+
const inbox = findInbox(inboxes, pos[1]);
|
|
123
|
+
if (!inbox) { out.err(`No inbox matches "${pos[1]}". See them all: atris mail inboxes`); return 1; }
|
|
124
|
+
|
|
125
|
+
if (sub === 'read') {
|
|
126
|
+
const limit = flags.limit == null ? 10 : Number(flags.limit);
|
|
127
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) { out.err('--limit must be a number from 1 to 200'); return 1; }
|
|
128
|
+
const res = await request(`${base}/inboxes/${encodeURIComponent(inbox.id)}/messages?limit=${limit}`, { method: 'GET', token });
|
|
129
|
+
if (!res || !res.ok) { out.err(`Could not read ${inbox.email_address}: ${errorText(res)}`); return 1; }
|
|
130
|
+
const rows = Array.isArray(res.data) ? res.data : [];
|
|
131
|
+
out.log(flags.json ? JSON.stringify(rows) : renderMessages(inbox, rows));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// send
|
|
136
|
+
const to = typeof flags.to === 'string' ? flags.to.trim() : '';
|
|
137
|
+
const subject = typeof flags.subject === 'string' ? flags.subject : '';
|
|
138
|
+
const text = typeof flags.body === 'string' ? flags.body : '';
|
|
139
|
+
if (!to || !subject || !text) { out.err('send needs --to, --subject, and --body'); return 1; }
|
|
140
|
+
const preview = `From: ${inbox.email_address}\nTo: ${to}\nSubject: ${subject}\n\n${text}\n`;
|
|
141
|
+
if (flags.dry_run === true) {
|
|
142
|
+
out.log(`${preview}\nDry run: nothing was sent.`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
// Only a bare --yes (or -y) skips the question; the parser refuses --yes=<anything>.
|
|
146
|
+
if (flags.yes !== true) {
|
|
147
|
+
out.log(preview);
|
|
148
|
+
const ok = await (deps.confirm || askYesNo)('Send this email? (y/N) ');
|
|
149
|
+
if (!ok) {
|
|
150
|
+
out.log(process.stdin.isTTY || deps.confirm ? 'Not sent.' : 'Not sent. Add --yes to send without asking.');
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const res = await request(`${base}/inboxes/${encodeURIComponent(inbox.id)}/messages/send`, {
|
|
155
|
+
method: 'POST', token, body: { to, subject, text }, retries: 0, timeoutMs: 30000,
|
|
156
|
+
});
|
|
157
|
+
if (!res || !res.ok) { out.err(`Not sent: ${errorText(res)}`); return 1; }
|
|
158
|
+
if (flags.json) out.log(JSON.stringify(res.data));
|
|
159
|
+
else out.log(`Sent from ${inbox.email_address} to ${to}.`);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { mailCommand, findInbox, renderInboxes, renderMessages };
|
package/commands/mission.js
CHANGED
|
@@ -7,6 +7,7 @@ const readline = require('readline');
|
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
8
|
const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
|
|
9
9
|
const { memberProcessPrompt } = require('../lib/member-context');
|
|
10
|
+
const { memberIdentityLine, missionObjectiveLine } = require('../lib/member-identity');
|
|
10
11
|
const {
|
|
11
12
|
compactErrorPayload,
|
|
12
13
|
compactSuccessPayload,
|
|
@@ -8634,7 +8635,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8634
8635
|
`# Mission Tick ${tickIndex}/${maxTicks}`,
|
|
8635
8636
|
...pingLines,
|
|
8636
8637
|
``,
|
|
8637
|
-
|
|
8638
|
+
missionObjectiveLine(mission.objective),
|
|
8638
8639
|
`**Owner:** ${mission.owner}`,
|
|
8639
8640
|
`**Lane:** ${frozen.lane}`,
|
|
8640
8641
|
`**Cadence:** ${mission.cadence}`,
|
|
@@ -8648,7 +8649,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
|
|
|
8648
8649
|
``,
|
|
8649
8650
|
`## Your task`,
|
|
8650
8651
|
`Do ONE increment of work toward the stop condition. ONE. No more.`,
|
|
8651
|
-
`-
|
|
8652
|
+
`- ${memberIdentityLine(mission.owner)} Read atris/team/${mission.owner}/MEMBER.md (and SOUL.md if present) before acting, work in that identity, inside its scope and stop rules. After your work, append what you did and what you learned to atris/team/${mission.owner}/logs/<today's date>.md.`,
|
|
8652
8653
|
`- FIRST: inspect current mission/task state before acting. Read the relevant files, run \`atris mission status ${mission.id}\`, \`git status\`, or \`atris task list\` as needed so you know what's already done.`,
|
|
8653
8654
|
`- Pick the smallest concrete action that moves the mission forward.`,
|
|
8654
8655
|
`- Before acting, state your single next move in one sentence.`,
|
package/commands/stream.js
CHANGED
|
@@ -589,12 +589,33 @@ function worktreeTask(root, deps) {
|
|
|
589
589
|
return worktreeConfig(root, deps).task;
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
// The branches a side copy starts from. A commit reachable from any of them
|
|
593
|
+
// was inherited, not made in the copy.
|
|
594
|
+
const WORKTREE_BASE_REFS = Object.freeze([
|
|
595
|
+
'refs/remotes/origin/HEAD',
|
|
596
|
+
'refs/remotes/origin/main',
|
|
597
|
+
'refs/remotes/origin/master',
|
|
598
|
+
'refs/heads/main',
|
|
599
|
+
'refs/heads/master',
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
function worktreeBaseRefs(root, deps) {
|
|
603
|
+
const result = runGit(root, ['for-each-ref', '--format=%(refname)', ...WORKTREE_BASE_REFS], deps);
|
|
604
|
+
if (result.status !== 0) return [];
|
|
605
|
+
return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter((ref) => WORKTREE_BASE_REFS.includes(ref));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Only a commit the copy added on top of its base counts. A fresh copy cut
|
|
609
|
+
// from someone else's commit has none, so its owner shows no work. A repo
|
|
610
|
+
// with no main, master, or origin default keeps the newest commit.
|
|
592
611
|
function collectWorktreeActivityEvents(root, deps, events) {
|
|
593
612
|
const roots = collectWorkspaceRoots(root, deps);
|
|
594
613
|
for (const item of roots) {
|
|
595
614
|
const owner = worktreeOwner(item.root, deps);
|
|
596
615
|
if (!owner) continue;
|
|
597
|
-
const
|
|
616
|
+
const bases = worktreeBaseRefs(item.root, deps);
|
|
617
|
+
const range = bases.length ? ['HEAD', '--not', ...bases] : [];
|
|
618
|
+
const log = runGit(item.root, ['log', '-1', '--format=%cI%x09%an%x09%s', ...range], deps);
|
|
598
619
|
if (log.status !== 0 || !String(log.stdout || '').trim()) continue;
|
|
599
620
|
const [ts, author, subject] = String(log.stdout || '').trim().split('\t');
|
|
600
621
|
pushEvent(events, {
|