atris 3.62.0 → 3.64.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/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +120 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +22 -2
- package/cli/atris_code.py +25 -1
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +242 -0
- package/commands/deck.js +2 -2
- package/commands/engine.js +122 -13
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/member.js +82 -15
- package/commands/mission.js +4 -1
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/task.js +91 -2
- package/commands/transactions.js +115 -0
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +24 -1
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +90 -0
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-registry.js +102 -18
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-engine.js +15 -1
- package/lib/member-scaffold.js +2 -0
- package/lib/member-type.js +27 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-runs.js +13 -2
- package/lib/runner-command.js +54 -3
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +122 -16
- package/scripts/det/ytrail-bench.js +215 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
package/commands/buy.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris buy: quote a purchase or trade, start the person's approval, check status.
|
|
4
|
+
//
|
|
5
|
+
// atris buy quote --kind ticket --event-url <vividseats url> --quantity 2
|
|
6
|
+
// atris buy approve ord_<id> --pick 1 --delivery-email sam@example.com
|
|
7
|
+
// atris buy status ord_<id>
|
|
8
|
+
//
|
|
9
|
+
// Nothing is ever charged by this command. Approve only starts the human step:
|
|
10
|
+
// a tap in their Link app, a payment page, or a reply to the confirm code Atris
|
|
11
|
+
// texts to their own phone. The code never comes back to this command.
|
|
12
|
+
|
|
13
|
+
const { apiRequestJson } = require('../utils/api');
|
|
14
|
+
const { loadCredentials } = require('../utils/auth');
|
|
15
|
+
const { NOT_LOGGED_IN, oneLine, printResult } = require('../lib/developer-api');
|
|
16
|
+
const { parseFlags, formatCents, untilText, errorFrom, commerceToken } = require('../lib/commerce');
|
|
17
|
+
|
|
18
|
+
const KINDS = ['ticket', 'shop', 'trade', 'flight'];
|
|
19
|
+
const NOTHING_CHARGED = 'Nothing is charged until the person approves it themselves.';
|
|
20
|
+
const CONTROL_FLAGS = new Set(['json', 'help', 'intent', 'details']);
|
|
21
|
+
const TRADE_CONFIRM = 'We texted you a confirm code. Reply to that text to place the trade.';
|
|
22
|
+
|
|
23
|
+
function showBuyHelp(log = console.log) {
|
|
24
|
+
log(`usage: atris buy <quote|approve|status> [flags] [--json]
|
|
25
|
+
|
|
26
|
+
get a real quote, then hand the person the approval step. nothing is charged
|
|
27
|
+
until the person approves it themselves (Link app, payment page, or a text
|
|
28
|
+
from their own phone).
|
|
29
|
+
|
|
30
|
+
atris buy quote --kind ticket|shop|trade|flight [fields] [--intent '<json>']
|
|
31
|
+
atris buy approve <id> [fields] [--details '<json>']
|
|
32
|
+
atris buy status <id>
|
|
33
|
+
|
|
34
|
+
quote fields (flags, kebab-case is fine):
|
|
35
|
+
ticket --event-url <vividseats.com url> --quantity N [--min-each-usd N] [--max-each-usd N]
|
|
36
|
+
shop --query "<item>" | --url <product url> [--quantity N]
|
|
37
|
+
trade --side buy|sell --symbol VTI (--quantity N | --dollars N) [--order-type market|limit --limit-price N]
|
|
38
|
+
flight not wired yet, quotes are refused
|
|
39
|
+
|
|
40
|
+
approve fields:
|
|
41
|
+
ticket --pick 1 --delivery-email you@example.com
|
|
42
|
+
shop --delivery-email you@example.com [--options '<json>'] [--address '<json>'] [--accept-extras]
|
|
43
|
+
trade none. Atris texts the person a confirm code; they reply to that text.
|
|
44
|
+
|
|
45
|
+
examples:
|
|
46
|
+
atris buy quote --kind trade --side buy --symbol VTI --dollars 25
|
|
47
|
+
atris buy quote --intent '{"kind":"shop","query":"uniqlo supima crew tee navy"}'
|
|
48
|
+
atris buy approve ord_1234 --pick 1 --delivery-email sam@example.com
|
|
49
|
+
atris buy status ord_1234 --json`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fieldsFrom(flags) {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
55
|
+
if (!CONTROL_FLAGS.has(key)) out[key] = value;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function jsonFlag(flags, name) {
|
|
61
|
+
if (flags[name] == null) return { ok: true, value: {} };
|
|
62
|
+
const value = flags[name];
|
|
63
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return { ok: true, value };
|
|
64
|
+
return { ok: false, error: `--${name} must be a JSON object, like --${name} '{"kind":"shop","query":"navy tee"}'` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function approvalLines(approval) {
|
|
68
|
+
if (!approval || typeof approval !== 'object') return [];
|
|
69
|
+
const lines = [];
|
|
70
|
+
// The backend sends trade confirm codes only to the person's own texts.
|
|
71
|
+
if (approval.how === 'text_confirm') return [TRADE_CONFIRM];
|
|
72
|
+
if (approval.url) {
|
|
73
|
+
const label = approval.how === 'connect_link' ? 'Connect Link first'
|
|
74
|
+
: approval.how === 'payment_page' ? 'Pay on this page'
|
|
75
|
+
: 'Approve in Link';
|
|
76
|
+
lines.push(`${label}: ${approval.url}`);
|
|
77
|
+
}
|
|
78
|
+
if (approval.text) lines.push(approval.text);
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function moneyLines(data) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
if (data.total_cents != null) lines.push(`Total: ${formatCents(data.total_cents, data.currency)}`);
|
|
85
|
+
if (data.fee_cents != null) lines.push(`Atris fee: ${formatCents(data.fee_cents, data.currency)}`);
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderQuote(data = {}, now = Date.now()) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
if (data.status !== 'quoted') return renderOutcome(data, now);
|
|
92
|
+
lines.push(data.summary || 'Quote ready.');
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push(...moneyLines(data));
|
|
95
|
+
if (data.expires_at) lines.push(`Expires: ${data.expires_at} (${untilText(data.expires_at, now)})`);
|
|
96
|
+
if (data.id) lines.push(`Quote id: ${data.id}`);
|
|
97
|
+
if (data.product_url) lines.push(`Product: ${data.product_url}`);
|
|
98
|
+
const choices = data.options && typeof data.options === 'object' && !Array.isArray(data.options)
|
|
99
|
+
? Object.entries(data.options) : [];
|
|
100
|
+
if (choices.length) {
|
|
101
|
+
lines.push('Choices:');
|
|
102
|
+
for (const [name, values] of choices) {
|
|
103
|
+
lines.push(` ${name}: ${Array.isArray(values) ? values.join(', ') : oneLine(values)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (data.note) lines.push('', String(data.note));
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(NOTHING_CHARGED);
|
|
109
|
+
const approval = data.approval && data.approval.text ? data.approval.text : '';
|
|
110
|
+
if (approval) lines.push(approval);
|
|
111
|
+
if (data.id) {
|
|
112
|
+
const hints = data.approve_with && typeof data.approve_with === 'object'
|
|
113
|
+
? Object.keys(data.approve_with).map((key) => `--${key.replace(/_/g, '-')} <${key}>`).join(' ')
|
|
114
|
+
: '';
|
|
115
|
+
lines.push(`Once they say yes: atris buy approve ${data.id}${hints ? ` ${hints}` : ''}`);
|
|
116
|
+
}
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderOutcome(data = {}, now = Date.now()) {
|
|
121
|
+
const lines = [];
|
|
122
|
+
const status = String(data.status || '');
|
|
123
|
+
if (status === 'pending_approval') {
|
|
124
|
+
lines.push(data.message || 'Waiting for the person to approve.');
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push(...moneyLines(data));
|
|
127
|
+
if (data.expires_at) lines.push(`Expires: ${data.expires_at} (${untilText(data.expires_at, now)})`);
|
|
128
|
+
if (data.id) lines.push(`Id: ${data.id}`);
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('Next, the person does this themselves:');
|
|
131
|
+
for (const line of approvalLines(data.approval)) lines.push(` ${line}`);
|
|
132
|
+
if (data.fee_card && data.fee_card.url) lines.push(` Fee card: ${data.fee_card.url}`);
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('Nothing has been charged yet. Money moves only after the person acts themselves.');
|
|
135
|
+
if (data.id) lines.push(`Check it: atris buy status ${data.id}`);
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
if (status === 'needs_setup') {
|
|
139
|
+
lines.push(data.message || 'Setup needed first.');
|
|
140
|
+
for (const line of approvalLines(data.approval)) lines.push(` ${line}`);
|
|
141
|
+
lines.push('Nothing was charged.');
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
if (status === 'needs_input') {
|
|
145
|
+
lines.push(data.message || 'More details needed.');
|
|
146
|
+
if (Array.isArray(data.fields) && data.fields.length) lines.push(`Needed: ${data.fields.join(', ')}`);
|
|
147
|
+
if (data.extra_usd != null) lines.push(`Extra: $${data.extra_usd}`);
|
|
148
|
+
if (data.total_usd != null) lines.push(`New total: $${data.total_usd}`);
|
|
149
|
+
lines.push('Nothing was charged.');
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
if (status === 'refused') {
|
|
153
|
+
lines.push(data.message || 'That did not go through.');
|
|
154
|
+
if (!/nothing was/i.test(String(data.message || ''))) lines.push('Nothing was charged.');
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
return data.message || oneLine(data);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderStatus(data = {}) {
|
|
161
|
+
const lines = [];
|
|
162
|
+
lines.push(`${data.id || ''} ${data.kind || ''} ${String(data.state || 'unknown').toUpperCase()}`.trim());
|
|
163
|
+
if (data.what) lines.push(`What: ${data.what}`);
|
|
164
|
+
if (data.amount_cents != null) lines.push(`Amount: ${formatCents(data.amount_cents, data.currency)}`);
|
|
165
|
+
if (data.when) lines.push(`When: ${data.when}`);
|
|
166
|
+
if (data.detail) lines.push(`Detail: ${data.detail}`);
|
|
167
|
+
if (data.counterparty) lines.push(`With: ${data.counterparty}`);
|
|
168
|
+
if (data.receipt_url) lines.push(`Receipt: ${data.receipt_url}`);
|
|
169
|
+
if (data.fee_payment_url) lines.push(`Pay the Atris fee: ${data.fee_payment_url}`);
|
|
170
|
+
const approval = approvalLines(data.approval);
|
|
171
|
+
if (approval.length) {
|
|
172
|
+
lines.push('Waiting on the person:');
|
|
173
|
+
for (const line of approval) lines.push(` ${line}`);
|
|
174
|
+
}
|
|
175
|
+
if (data.state === 'unknown') lines.push('Money may have moved. Check the store or Robinhood app. Do not buy again to fix it.');
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildRequest(sub, pos, flags) {
|
|
180
|
+
if (sub === 'quote') {
|
|
181
|
+
const base = jsonFlag(flags, 'intent');
|
|
182
|
+
if (!base.ok) return base;
|
|
183
|
+
const body = { ...base.value, ...fieldsFrom(flags) };
|
|
184
|
+
const kind = String(body.kind || '').toLowerCase();
|
|
185
|
+
if (!KINDS.includes(kind)) return { ok: false, error: `--kind must be one of ${KINDS.join(', ')}` };
|
|
186
|
+
body.kind = kind;
|
|
187
|
+
return { ok: true, path: '/commerce/quote', method: 'POST', body };
|
|
188
|
+
}
|
|
189
|
+
if (sub === 'approve' || sub === 'status') {
|
|
190
|
+
const id = String(pos[1] || '').trim();
|
|
191
|
+
if (!id) return { ok: false, error: `usage: atris buy ${sub} <id>` };
|
|
192
|
+
const path = `/commerce/${encodeURIComponent(id)}`;
|
|
193
|
+
if (sub === 'status') return { ok: true, path, method: 'GET' };
|
|
194
|
+
const base = jsonFlag(flags, 'details');
|
|
195
|
+
if (!base.ok) return base;
|
|
196
|
+
return { ok: true, path: `${path}/approve`, method: 'POST', body: { ...base.value, ...fieldsFrom(flags) } };
|
|
197
|
+
}
|
|
198
|
+
return { ok: false, error: 'usage: atris buy <quote|approve|status>. run: atris buy --help' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function buyCommand(args = [], deps = {}) {
|
|
202
|
+
const io = { log: deps.log || console.log, err: deps.err || console.error };
|
|
203
|
+
const { flags, pos } = parseFlags(args);
|
|
204
|
+
const sub = String(pos[0] || '').toLowerCase();
|
|
205
|
+
if (!sub || sub === 'help' || flags.help) {
|
|
206
|
+
showBuyHelp(io.log);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const json = flags.json === true;
|
|
211
|
+
const fail = (error, status) => {
|
|
212
|
+
printResult({ json, ok: false, error, payload: { ok: false, error, ...(status ? { status } : {}) } }, io);
|
|
213
|
+
return 1;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const req = buildRequest(sub, pos, flags);
|
|
217
|
+
if (!req.ok) return fail(req.error);
|
|
218
|
+
|
|
219
|
+
const load = deps.loadCredentials || loadCredentials;
|
|
220
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
221
|
+
const token = commerceToken(load(), 'commerce:quote', deps.now ? deps.now() : Date.now());
|
|
222
|
+
if (!token) return fail(NOT_LOGGED_IN);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// No retries: a quote or approval must never be sent twice by accident.
|
|
226
|
+
const options = { method: req.method, token, retries: 0 };
|
|
227
|
+
if (req.body) options.body = req.body;
|
|
228
|
+
const result = await request(req.path, options);
|
|
229
|
+
if (!result || !result.ok) return fail(errorFrom(result), result && result.status);
|
|
230
|
+
const data = result.data || {};
|
|
231
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
232
|
+
const text = sub === 'quote' ? renderQuote(data, now)
|
|
233
|
+
: sub === 'approve' ? renderOutcome(data, now)
|
|
234
|
+
: renderStatus(data);
|
|
235
|
+
printResult({ json, ok: true, text, payload: { ok: true, ...data } }, io);
|
|
236
|
+
return 0;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return fail(oneLine(error && error.message));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { buyCommand };
|
package/commands/deck.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const https = require('https');
|
|
18
|
-
const os = require('os');
|
|
19
18
|
const path = require('path');
|
|
19
|
+
const { stateHome } = require('../lib/state-home');
|
|
20
20
|
const { hasFlag } = require('../lib/arg-parser');
|
|
21
21
|
const { buildDeck, THEMES, notesRequests } = require('../lib/slides-deck');
|
|
22
22
|
const {
|
|
@@ -34,7 +34,7 @@ const BASE = 'api.atris.ai';
|
|
|
34
34
|
const PFX = '/api/integrations/google-slides';
|
|
35
35
|
|
|
36
36
|
function token() {
|
|
37
|
-
try { return require(path.join(
|
|
37
|
+
try { return require(path.join(stateHome(), '.atris/credentials.json')).token; }
|
|
38
38
|
catch { return null; }
|
|
39
39
|
}
|
|
40
40
|
|
package/commands/engine.js
CHANGED
|
@@ -69,6 +69,8 @@ const {
|
|
|
69
69
|
readEngineRegistry,
|
|
70
70
|
resolveEngineForRole,
|
|
71
71
|
resolveEngineForRoleRanked,
|
|
72
|
+
resolveNotesJob,
|
|
73
|
+
jobTakesAnyEngine,
|
|
72
74
|
requireEngineBin,
|
|
73
75
|
engineDoctorReport,
|
|
74
76
|
engineFailureHealthStatus,
|
|
@@ -1000,10 +1002,41 @@ function rosterPickEntry(value) {
|
|
|
1000
1002
|
|
|
1001
1003
|
const WORKER_SKIP_WORDS = Object.freeze({ 'not ready': 'down', expired: 'expired', 'bad model': 'names a model it cannot run' });
|
|
1002
1004
|
|
|
1005
|
+
// A worker whose end date is this many days away or fewer gets a heads-up.
|
|
1006
|
+
const ENDS_SOON_DAYS = 7;
|
|
1007
|
+
const RENEW_COMMAND = 'atris engine roster confirm';
|
|
1008
|
+
|
|
1009
|
+
// Whole local days from today to a worker's end date: 0 on its last day,
|
|
1010
|
+
// negative once it has ended, null when it has no valid end date.
|
|
1011
|
+
function endsInDays(until, now = new Date()) {
|
|
1012
|
+
const end = parseRosterUntil(until);
|
|
1013
|
+
if (!end) return null;
|
|
1014
|
+
const date = new Date(now);
|
|
1015
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
1016
|
+
const today = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1017
|
+
return Math.round((end.getTime() - today.getTime()) / 86400000);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function endsSoon(days) {
|
|
1021
|
+
return days !== null && days >= 0 && days <= ENDS_SOON_DAYS;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function endsInText(days) {
|
|
1025
|
+
if (days === 0) return 'ends today';
|
|
1026
|
+
if (days === 1) return 'ends tomorrow';
|
|
1027
|
+
return `ends in ${days} days`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function shortDay(until) {
|
|
1031
|
+
const date = parseRosterUntil(until);
|
|
1032
|
+
return date ? date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toLowerCase() : '';
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1003
1035
|
// One worker of a job's team as the view shows it: what it runs, and whether
|
|
1004
1036
|
// it leads now, backs up, or is skipped and why.
|
|
1005
|
-
function workerRow(step, index, leadIndex) {
|
|
1037
|
+
function workerRow(step, index, leadIndex, now = new Date()) {
|
|
1006
1038
|
const { worker } = step;
|
|
1039
|
+
const endsIn = endsInDays(worker.until, now);
|
|
1007
1040
|
const runs = worker.error ? null : engineRunsView(worker.engine, { model: worker.model || '', effort: worker.effort || '' });
|
|
1008
1041
|
const status = index === leadIndex ? 'leads' : step.skip ? 'skipped' : 'backup';
|
|
1009
1042
|
const why = step.skip === 'bad line' ? `bad line: ${worker.error}`
|
|
@@ -1016,6 +1049,8 @@ function workerRow(step, index, leadIndex) {
|
|
|
1016
1049
|
max_seconds: Number(worker.max_seconds) > 0 ? Number(worker.max_seconds) : null,
|
|
1017
1050
|
prep: worker.prep ? rosterJobLabel(worker.prep) : null,
|
|
1018
1051
|
until: worker.until || null,
|
|
1052
|
+
ends_in_days: endsIn,
|
|
1053
|
+
expiring_soon: endsSoon(endsIn),
|
|
1019
1054
|
line: worker.line || null,
|
|
1020
1055
|
...(worker.error ? { text: worker.text || '' } : {}),
|
|
1021
1056
|
status,
|
|
@@ -1041,7 +1076,7 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1041
1076
|
const decided = rosterDecided(resolved.source) && (!custom || resolved.job === job);
|
|
1042
1077
|
const source = decided ? resolved.source : ROSTER_LAYERS.find((scope) => picks[scope]) || null;
|
|
1043
1078
|
const pick = source ? picks[source] : null;
|
|
1044
|
-
const walk = pick ? rosterWorkerWalk(pick, role, registry, now) : [];
|
|
1079
|
+
const walk = pick ? rosterWorkerWalk(pick, role, registry, now, { anyEngine: jobTakesAnyEngine(key) }) : [];
|
|
1045
1080
|
const leadIndex = decided && resolved.source === source ? walk.findIndex((step) => !step.skip) : -1;
|
|
1046
1081
|
const good = walk.map((step, index) => ({ step, index })).filter(({ step }) => !step.worker.error);
|
|
1047
1082
|
const first = good[0] || null;
|
|
@@ -1055,6 +1090,7 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1055
1090
|
: good[1] && leadIndex === good[1].index ? 'backup'
|
|
1056
1091
|
: 'later';
|
|
1057
1092
|
const kind = Object.keys(ENGINE_JOBS).find((name) => ENGINE_JOBS[name] === role);
|
|
1093
|
+
const pickEndsIn = pick ? endsInDays(pick.until, now) : null;
|
|
1058
1094
|
// What the job really runs: its first two good workers, each with its own
|
|
1059
1095
|
// model and effort, or the router's engine when no line decides.
|
|
1060
1096
|
const runsOf = (entry) => (entry ? engineRunsView(entry.step.worker.engine, { model: entry.step.worker.model || '', effort: entry.step.worker.effort || '' }) : null);
|
|
@@ -1076,12 +1112,14 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1076
1112
|
model: resolved.engine && resolved.engine.roster_model ? resolved.engine.roster_model : null,
|
|
1077
1113
|
effort: resolved.engine && resolved.engine.roster_effort ? resolved.engine.roster_effort : null,
|
|
1078
1114
|
max_seconds: pick && Number(pick.max_seconds) > 0 ? Number(pick.max_seconds) : null,
|
|
1115
|
+
ends_in_days: pickEndsIn,
|
|
1116
|
+
expiring_soon: endsSoon(pickEndsIn),
|
|
1079
1117
|
prep: first && first.step.worker.prep ? rosterJobLabel(first.step.worker.prep) : null,
|
|
1080
1118
|
backup_prep: good[1] && good[1].step.worker.prep ? rosterJobLabel(good[1].step.worker.prep) : null,
|
|
1081
1119
|
runs: runs || nowRuns,
|
|
1082
1120
|
backup_runs: runsOf(good[1]),
|
|
1083
1121
|
now_runs: nowRuns,
|
|
1084
|
-
workers: walk.map((step, index) => workerRow(step, index, leadIndex)),
|
|
1122
|
+
workers: walk.map((step, index) => workerRow(step, index, leadIndex, now)),
|
|
1085
1123
|
lead,
|
|
1086
1124
|
status,
|
|
1087
1125
|
...(status === 'cooling' ? { cooling: first.step.cooling.text } : {}),
|
|
@@ -1114,8 +1152,11 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1114
1152
|
const state = readRosterState(root, { now });
|
|
1115
1153
|
const jobs = jobRosterView(root, now, state);
|
|
1116
1154
|
const team = teamRosterView(root, { now, rosterState: state });
|
|
1155
|
+
const expiring = expiringWorkers(jobs);
|
|
1117
1156
|
return {
|
|
1118
1157
|
jobs,
|
|
1158
|
+
expiring,
|
|
1159
|
+
renew_command: expiring.length ? RENEW_COMMAND : null,
|
|
1119
1160
|
team: team.rows,
|
|
1120
1161
|
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1121
1162
|
files: {
|
|
@@ -1127,12 +1168,52 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1127
1168
|
};
|
|
1128
1169
|
}
|
|
1129
1170
|
|
|
1130
|
-
|
|
1171
|
+
// Every worker, in any job, whose end date is today or within the next
|
|
1172
|
+
// seven days. Workers already past their date show on their own lines. A
|
|
1173
|
+
// job set for this shell only is skipped: roster confirm does not renew it.
|
|
1174
|
+
function expiringWorkers(jobs) {
|
|
1175
|
+
const list = [];
|
|
1176
|
+
for (const row of jobs || []) {
|
|
1177
|
+
if (row.session_pick) continue;
|
|
1178
|
+
for (const worker of row.workers || []) {
|
|
1179
|
+
if (!worker.expiring_soon) continue;
|
|
1180
|
+
list.push({
|
|
1181
|
+
job: row.job,
|
|
1182
|
+
engine: worker.engine,
|
|
1183
|
+
model: worker.model,
|
|
1184
|
+
tool: worker.runs ? worker.runs.text : worker.engine,
|
|
1185
|
+
until: worker.until,
|
|
1186
|
+
ends_in_days: worker.ends_in_days,
|
|
1187
|
+
from: row.from,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return list;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function untilText(worker, days = null) {
|
|
1131
1195
|
const until = parseRosterUntil(worker.until);
|
|
1132
|
-
if (until) return `until ${until
|
|
1196
|
+
if (until) return `until ${shortDay(worker.until)}${endsSoon(days) ? `, ${endsInText(days)}` : ''}`;
|
|
1133
1197
|
return worker.never_expires ? 'no end date' : 'until no valid date';
|
|
1134
1198
|
}
|
|
1135
1199
|
|
|
1200
|
+
// One line under the jobs when any worker ends within seven days, naming
|
|
1201
|
+
// each one and the command that keeps it going.
|
|
1202
|
+
function renderExpiringLine(expiring) {
|
|
1203
|
+
if (!expiring || !expiring.length) return '';
|
|
1204
|
+
const names = expiring.map((item) => `${item.tool} on ${item.job} ${endsInText(item.ends_in_days)}`);
|
|
1205
|
+
const them = expiring.length === 1 ? 'it' : 'them';
|
|
1206
|
+
return `heads up: ${names.join(', ')}. to keep ${them} 30 more days, run: ${RENEW_COMMAND}`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// The same heads-up, short enough for one boot line.
|
|
1210
|
+
function bootExpiringLine(expiring) {
|
|
1211
|
+
if (!expiring || !expiring.length) return '';
|
|
1212
|
+
const [first] = expiring;
|
|
1213
|
+
const more = expiring.length > 1 ? ` and ${expiring.length - 1} more` : '';
|
|
1214
|
+
return `${first.tool} on ${first.job} ${endsInText(first.ends_in_days)}${more}. renew: ${RENEW_COMMAND}`;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1136
1217
|
// Every worker under its job, numbered in order, when the job has more than
|
|
1137
1218
|
// the lead and one backup (the job's own line already shows those two).
|
|
1138
1219
|
function renderWorkerLines(row) {
|
|
@@ -1140,8 +1221,11 @@ function renderWorkerLines(row) {
|
|
|
1140
1221
|
const width = Math.max(...row.workers.map((worker) => (worker.runs ? worker.runs.text : worker.text || '').length));
|
|
1141
1222
|
return row.workers.map((worker, index) => {
|
|
1142
1223
|
const what = (worker.runs ? worker.runs.text : worker.text || '').padEnd(width);
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1224
|
+
const ended = worker.status === 'skipped' && worker.why === 'expired' && worker.ends_in_days !== null;
|
|
1225
|
+
const until = !worker.until || ended ? '' : `until ${worker.until}${worker.expiring_soon ? `, ${endsInText(worker.ends_in_days)}` : ''}`;
|
|
1226
|
+
const extras = [worker.max_seconds ? rosterMaxText(worker.max_seconds) : '', worker.prep ? `prepped by ${worker.prep}` : '', until].filter(Boolean);
|
|
1227
|
+
const state = worker.status === 'leads' ? 'leads now' : worker.status === 'backup' ? 'backup'
|
|
1228
|
+
: ended ? `ended ${shortDay(worker.until)}, skipped` : `skipped, ${worker.why}`;
|
|
1145
1229
|
return ` ${index + 1}. ${what} ${[state, ...extras].join(', ')}`.trimEnd();
|
|
1146
1230
|
});
|
|
1147
1231
|
}
|
|
@@ -1165,9 +1249,10 @@ function renderJobRoster(rows) {
|
|
|
1165
1249
|
const owner = row.runs.text.padEnd(ownerWidth);
|
|
1166
1250
|
const backup = row.backup_runs ? `backup ${row.backup_runs.text}${row.backup_prep ? ` prepped by ${row.backup_prep}` : ''}` : 'no backup';
|
|
1167
1251
|
const cap = `${row.max_seconds ? `${rosterMaxText(row.max_seconds)}, ` : ''}${row.prep ? `prepped by ${row.prep}, ` : ''}`;
|
|
1168
|
-
const date = untilText(row.pick);
|
|
1252
|
+
const date = untilText(row.pick, row.ends_in_days);
|
|
1169
1253
|
const fallback = row.lead === 'backup' ? 'using backup' : row.lead === 'later' ? `using ${nowText}` : fallsTo;
|
|
1170
|
-
const
|
|
1254
|
+
const ended = row.ends_in_days !== null && row.ends_in_days !== undefined ? `ended ${shortDay(row.pick.until)}, skipped` : 'expired';
|
|
1255
|
+
const status = row.status === 'expired' ? `${ended}, ${fallback}`
|
|
1171
1256
|
: row.status === 'cooling' ? `${row.cooling}, ${fallback}`
|
|
1172
1257
|
: row.status === 'not ready' ? `not ready, ${fallback}`
|
|
1173
1258
|
: date;
|
|
@@ -1195,7 +1280,8 @@ function renderRosterWarnings(warnings) {
|
|
|
1195
1280
|
}
|
|
1196
1281
|
|
|
1197
1282
|
function renderRosterReport(report) {
|
|
1198
|
-
|
|
1283
|
+
const jobs = [renderJobRoster(report.jobs), renderExpiringLine(report.expiring)].filter(Boolean).join('\n');
|
|
1284
|
+
return [jobs, renderTeamRoster(report.team), renderRosterWarnings(report.warnings)]
|
|
1199
1285
|
.filter(Boolean)
|
|
1200
1286
|
.join('\n\n');
|
|
1201
1287
|
}
|
|
@@ -1530,9 +1616,30 @@ function parseSetFlag(args) {
|
|
|
1530
1616
|
return '';
|
|
1531
1617
|
}
|
|
1532
1618
|
|
|
1533
|
-
|
|
1619
|
+
// One worker as a person reads it: the tool, then its model and effort.
|
|
1620
|
+
function notesWorkerText(worker) {
|
|
1621
|
+
const details = [worker.model, worker.effort ? `effort ${worker.effort}` : '', worker.installed ? '' : 'not installed'].filter(Boolean);
|
|
1622
|
+
return details.length ? `${worker.engine} (${details.join(', ')})` : worker.engine;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// The notes job: the roster's pick when one is set, else the built-in
|
|
1626
|
+
// default. Always answers, so scripts can call it with a short timeout.
|
|
1627
|
+
function runNotesResolve(json, root, now) {
|
|
1628
|
+
const answer = resolveNotesJob(root, { now });
|
|
1629
|
+
if (json) {
|
|
1630
|
+
console.log(JSON.stringify(answer));
|
|
1631
|
+
return 0;
|
|
1632
|
+
}
|
|
1633
|
+
const backup = answer.backup.length ? `backup ${answer.backup.map(notesWorkerText).join(', then ')}` : 'no backup';
|
|
1634
|
+
const where = answer.source === 'roster' ? `from ${answer.from}` : `built-in default${answer.skipped ? ` (${answer.skipped})` : ''}`;
|
|
1635
|
+
console.log(`notes: ${notesWorkerText(answer)}, ${backup}, ${where}`);
|
|
1636
|
+
return 0;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function runResolveCommand(args, root, now) {
|
|
1534
1640
|
const json = args.includes('--json');
|
|
1535
1641
|
const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
|
|
1642
|
+
if (role && rosterJobKey(role) === 'notes') return runNotesResolve(json, root, now);
|
|
1536
1643
|
if (!role) {
|
|
1537
1644
|
const message = `usage: atris engine resolve <role> or <job>; roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or your own`;
|
|
1538
1645
|
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
@@ -2133,7 +2240,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2133
2240
|
}
|
|
2134
2241
|
|
|
2135
2242
|
if (sub === 'resolve') {
|
|
2136
|
-
return runResolveCommand(scope.args.slice(scope.args.indexOf('resolve') + 1), root);
|
|
2243
|
+
return runResolveCommand(scope.args.slice(scope.args.indexOf('resolve') + 1), root, deps.now);
|
|
2137
2244
|
}
|
|
2138
2245
|
|
|
2139
2246
|
if (sub === 'health') {
|
|
@@ -2160,7 +2267,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2160
2267
|
if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
2161
2268
|
console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
|
|
2162
2269
|
console.log('\n long read-only asks: atris engine ask "..." --engine agy --timeout <seconds>\n quick asks default to 120 seconds; explicit jobs allow up to 3600\n follow live work with atris engine watch latest');
|
|
2163
|
-
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2270
|
+
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job;\n notes always answers: its roster pick, else agy then haiku\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2164
2271
|
return 0;
|
|
2165
2272
|
}
|
|
2166
2273
|
if (isFreshWorkspace(root)) {
|
|
@@ -2210,6 +2317,8 @@ module.exports = {
|
|
|
2210
2317
|
roster,
|
|
2211
2318
|
rosterReport,
|
|
2212
2319
|
jobRosterView,
|
|
2320
|
+
expiringWorkers,
|
|
2321
|
+
bootExpiringLine,
|
|
2213
2322
|
engineRegistryFile,
|
|
2214
2323
|
readEngineRegistry,
|
|
2215
2324
|
resolveEngineForRole,
|
package/commands/feedback.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const { stateHome } = require('../lib/state-home');
|
|
20
21
|
const { loadCredentials } = require('../utils/auth');
|
|
21
22
|
const { apiRequestJson } = require('../utils/api');
|
|
22
23
|
const {
|
|
@@ -297,8 +298,7 @@ function resolveBusinessArg(value) {
|
|
|
297
298
|
// Full UUID, trust it
|
|
298
299
|
if (/^[0-9a-f-]{32,}$/i.test(value)) return value;
|
|
299
300
|
// Otherwise treat as slug and look up in ~/.atris/businesses.json
|
|
300
|
-
const
|
|
301
|
-
const file = path.join(home, '.atris', 'businesses.json');
|
|
301
|
+
const file = path.join(stateHome(), '.atris', 'businesses.json');
|
|
302
302
|
if (!fs.existsSync(file)) return null;
|
|
303
303
|
try {
|
|
304
304
|
const map = JSON.parse(fs.readFileSync(file, 'utf8'));
|
package/commands/fork.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { loadCredentials } = require('../utils/auth');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
|
|
@@ -48,7 +48,7 @@ async function forkAtris() {
|
|
|
48
48
|
files = downloadResult.data.files;
|
|
49
49
|
} else {
|
|
50
50
|
// Fall back to local template at ~/.atris/templates/{template}/
|
|
51
|
-
const localTemplatePath = path.join(
|
|
51
|
+
const localTemplatePath = path.join(stateHome(), '.atris', 'templates', template);
|
|
52
52
|
if (fs.existsSync(localTemplatePath)) {
|
|
53
53
|
console.log(' API unavailable, using local template...');
|
|
54
54
|
files = readLocalTemplate(localTemplatePath, localTemplatePath);
|
package/commands/gm.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { stateHome } = require('../lib/state-home');
|
|
6
7
|
const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
|
|
7
8
|
|
|
8
9
|
const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
|
|
@@ -452,7 +453,7 @@ function feedPostId(post) {
|
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
function credentialsFileExists() {
|
|
455
|
-
return fs.existsSync(path.join(
|
|
456
|
+
return fs.existsSync(path.join(stateHome(), '.atris', 'credentials.json'));
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
async function fetchFeedEvents(workspaceRoot) {
|