atris 3.64.0 → 3.66.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.
@@ -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 };
@@ -9412,7 +9412,7 @@ async function memberCommand(subcommand, ...args) {
9412
9412
  // Subcommands that take a member name as args[0] otherwise treat `--help` as
9413
9413
  // a name and error with "Member '--help' not found". `create`/`new` handle
9414
9414
  // help themselves (with subcommand-specific usage), leave those alone.
9415
- const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run', 'install']);
9415
+ const HELP_AWARE_SUBCOMMANDS = new Set(['create', 'new', 'run', 'install', 'result']);
9416
9416
  if (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help')) {
9417
9417
  subcommand = undefined;
9418
9418
  }
@@ -9449,6 +9449,11 @@ async function memberCommand(subcommand, ...args) {
9449
9449
  return memberWake(args[0], ...args.slice(1));
9450
9450
  case 'run':
9451
9451
  return memberRun(args[0], ...args.slice(1));
9452
+ case 'result': {
9453
+ const code = require('../lib/member-result').memberResultCommand(args);
9454
+ if (code) process.exitCode = code;
9455
+ return undefined;
9456
+ }
9452
9457
  case 'ping':
9453
9458
  return memberPing(args[0], ...args.slice(1));
9454
9459
  case 'loop':
@@ -9498,6 +9503,7 @@ async function memberCommand(subcommand, ...args) {
9498
9503
  console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
9499
9504
  console.log(' wake <name> Read Mission state and decide tick/wait/ask/stop');
9500
9505
  console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
9506
+ console.log(' result <name> Newest run result: run record, receipt, artifacts, number [--since <iso>] [--json]');
9501
9507
  console.log(' loop <name|--all> Repeat wake on a bounded cadence with a no-overlap lease');
9502
9508
  console.log(' tick <name> Propose the next bounded experiment');
9503
9509
  console.log(' review <name> <id> Accept/discard an experiment with proof');
@@ -9533,6 +9539,7 @@ async function memberCommand(subcommand, ...args) {
9533
9539
  console.log(' atris member wake growth --json');
9534
9540
  console.log(' atris member run growth "improve onboarding proof" --verify "npm test" --minutes 30 --json');
9535
9541
  console.log(' atris member run growth --mission <mission-id> --max-ticks 1 --json');
9542
+ console.log(' atris member result growth --json');
9536
9543
  console.log(' atris member wake growth --execute --confirm-autonomy-policy');
9537
9544
  console.log(' atris member supervisor recommendations --json');
9538
9545
  console.log(' atris member objective-generator proposals --json');
@@ -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
- `**Objective:** ${mission.objective}`,
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
- `- You are the member "${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
+ `- ${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/orb.js CHANGED
@@ -42,7 +42,7 @@ function parseEngine(args) {
42
42
  }
43
43
 
44
44
  function engineInvocation(engine, prompt) {
45
- if (engine === 'codex') return { command: 'codex', args: ['exec', prompt] };
45
+ if (engine === 'codex') return { command: 'codex', args: ['exec', '--skip-git-repo-check', prompt] };
46
46
  if (engine === 'fast') return { command: 'ax', args: ['--fast', '--print', prompt] };
47
47
  return { command: 'claude', args: ['-p', prompt] };
48
48
  }
@@ -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 log = runGit(item.root, ['log', '-1', '--format=%cI%x09%an%x09%s'], deps);
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, {