atris 3.63.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/atris/skills/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +28 -2
- package/cli/atris_code.py +25 -1
- package/commands/agent-spawn.js +1 -0
- 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 +3 -3
- package/commands/deck.js +2 -2
- package/commands/engine.js +292 -65
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/hire.js +189 -0
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +47 -9
- package/commands/mission.js +4 -3
- 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/stream.js +22 -1
- package/commands/task.js +84 -2
- package/commands/team.js +355 -67
- package/commands/transactions.js +3 -3
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +2 -1
- package/lib/auto-accept-certified.js +83 -9
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/business-target.js +131 -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 +17 -2
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +101 -21
- 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-identity.js +56 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-models.js +11 -0
- package/lib/roster-runs.js +13 -2
- package/lib/roster-stale.js +241 -0
- package/lib/runner-command.js +22 -6
- 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/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -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 +61 -15
- package/scripts/det/ytrail-bench.js +2 -1
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Bench what users get from `atris x-search`, end to end.
|
|
5
|
+
// Usage: node scripts/det/xsearch-bench.js [--quick|--free] [--case <name>]
|
|
6
|
+
// default: every case, 2 paid searches (10 credits)
|
|
7
|
+
// --quick: topic plus the free cases (5 credits)
|
|
8
|
+
// --free: free cases only (0 credits)
|
|
9
|
+
// Prints one line per case and a final "bench: X/Y passed in Ns"; exits 1 if
|
|
10
|
+
// any case fails. Rows (with a `case` field) go to $XSEARCH_OUT_DIR/xsearch.jsonl,
|
|
11
|
+
// default ~/.atris/benchmarks, outside any git repo. A failed paid case also
|
|
12
|
+
// keeps its full output there, since it cost credits to get.
|
|
13
|
+
// bin/atris.js is resolved next to this file, so an installed copy benches itself.
|
|
14
|
+
|
|
15
|
+
const { spawn } = require('node:child_process');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const http = require('node:http');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { checkXPosts } = require('../../lib/x-post-check');
|
|
21
|
+
|
|
22
|
+
const ATRIS = path.join(__dirname, '..', '..', 'bin', 'atris.js');
|
|
23
|
+
const FIXTURE = path.join(__dirname, 'data', 'xsearch-answer.json');
|
|
24
|
+
const PAID_TIMEOUT_MS = 180000;
|
|
25
|
+
const STRANGER_TIMEOUT_MS = 60000;
|
|
26
|
+
const CHECKED_MIN = 0.6;
|
|
27
|
+
|
|
28
|
+
const CASES = [
|
|
29
|
+
{ name: 'topic', kind: 'paid', args: ['x-search', 'AI agent startups launch', '--limit', '5', '--days', '2'], minPosts: 3, quick: true },
|
|
30
|
+
{ name: 'person', kind: 'paid', args: ['x-search', 'person', '--name', 'Garry Tan', '--handle', 'garrytan'], minPosts: 1 },
|
|
31
|
+
{ name: 'stranger-logged-out', kind: 'stranger', args: ['x-search', 'AI agent startups launch', '--limit', '5', '--days', '2'], quick: true, free: true },
|
|
32
|
+
{ name: 'offline-check', kind: 'offline', quick: true, free: true },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function parseArgs(argv) {
|
|
36
|
+
const opts = { quick: false, free: false, only: null };
|
|
37
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
38
|
+
if (argv[i] === '--quick') opts.quick = true;
|
|
39
|
+
else if (argv[i] === '--free') opts.free = true;
|
|
40
|
+
else if (argv[i] === '--case') opts.only = argv[++i] || '';
|
|
41
|
+
else if (argv[i].startsWith('--case=')) opts.only = argv[i].slice('--case='.length);
|
|
42
|
+
else throw new Error(`unknown option: ${argv[i]}`);
|
|
43
|
+
}
|
|
44
|
+
return opts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function selectCases(opts, cases = CASES) {
|
|
48
|
+
if (opts.only !== null) {
|
|
49
|
+
const found = cases.filter((c) => c.name === opts.only);
|
|
50
|
+
if (!found.length) {
|
|
51
|
+
throw new Error(`unknown case: ${opts.only} (cases: ${cases.map((c) => c.name).join(', ')})`);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
if (opts.free) return cases.filter((c) => c.free);
|
|
56
|
+
return opts.quick ? cases.filter((c) => c.quick) : cases.slice();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// x-search prints "posts: N checked, N unverified, N unknown" at the end.
|
|
60
|
+
function parseTally(stdout) {
|
|
61
|
+
const match = String(stdout || '').match(/posts: (\d+) checked, (\d+) unverified, (\d+) unknown/);
|
|
62
|
+
if (!match) return null;
|
|
63
|
+
return { checked: Number(match[1]), unverified: Number(match[2]), unknown: Number(match[3]) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pure pass/fail rules. Each takes a finished run and returns { pass, note }.
|
|
67
|
+
// Posts = checked citations plus unverified quotes. Unknown (X could not be
|
|
68
|
+
// reached) counts neither way.
|
|
69
|
+
function postCount(tally) {
|
|
70
|
+
return tally ? tally.checked + tally.unverified : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function judgePaid({ status, tally, minPosts }) {
|
|
74
|
+
const posts = postCount(tally);
|
|
75
|
+
const share = posts ? tally.checked / posts : 0;
|
|
76
|
+
if (status !== 0) return { pass: false, posts, note: 'search failed' };
|
|
77
|
+
if (!tally) return { pass: false, posts, note: 'no posts found to check' };
|
|
78
|
+
if (posts < minPosts) return { pass: false, posts, note: `only ${posts} posts, wanted ${minPosts}` };
|
|
79
|
+
if (share < CHECKED_MIN) return { pass: false, posts, note: `only ${Math.round(share * 100)}% checked` };
|
|
80
|
+
return { pass: true, posts, note: `${Math.round(share * 100)}% checked` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function judgeStranger({ status, text, hits }) {
|
|
84
|
+
const told = /not signed in|atris login/i.test(String(text || ''));
|
|
85
|
+
const pass = told && typeof status === 'number' && status !== 0 && hits === 0;
|
|
86
|
+
let note = 'says to log in, nothing charged';
|
|
87
|
+
if (!told) note = 'did not say to log in';
|
|
88
|
+
else if (hits) note = `reached the server ${hits} time(s)`;
|
|
89
|
+
else if (status === 0) note = 'exited 0 without logging in';
|
|
90
|
+
return { pass, note };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function fixtureFetch(embeds, downId = null) {
|
|
94
|
+
return async (id) => {
|
|
95
|
+
if (id === downId) return { id, ok: false, error: 'timeout' };
|
|
96
|
+
if (!(id in embeds)) return { id, ok: false, error: 'not in fixture' };
|
|
97
|
+
const post = embeds[id];
|
|
98
|
+
return post ? { id, ok: true, post: { id, handle: post.handle, text: post.text, createdAt: null } } : { id, ok: true, post: null };
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Run the checker on the saved answer twice: once with X answering every
|
|
103
|
+
// lookup, once with one lookup timing out. No network.
|
|
104
|
+
async function runOfflineCheck(fixture = JSON.parse(fs.readFileSync(FIXTURE, 'utf8'))) {
|
|
105
|
+
const up = await checkXPosts({ content: fixture.content, citations: fixture.citations, fetchPost: fixtureFetch(fixture.embeds) });
|
|
106
|
+
const down = await checkXPosts({
|
|
107
|
+
content: fixture.content,
|
|
108
|
+
citations: fixture.citations,
|
|
109
|
+
fetchPost: fixtureFetch(fixture.embeds, fixture.expect.one_down.down_id),
|
|
110
|
+
});
|
|
111
|
+
const want = fixture.expect;
|
|
112
|
+
const upOk = Boolean(up)
|
|
113
|
+
&& up.tally.checked === want.all_up.checked
|
|
114
|
+
&& up.tally.unverified === want.all_up.unverified
|
|
115
|
+
&& up.tally.unknown === want.all_up.unknown
|
|
116
|
+
&& up.otherSources.length === want.all_up.other_sources;
|
|
117
|
+
const downOk = Boolean(down)
|
|
118
|
+
&& down.tally.checked === want.one_down.checked
|
|
119
|
+
&& down.tally.unverified === want.one_down.unverified
|
|
120
|
+
&& down.tally.unknown === want.one_down.unknown;
|
|
121
|
+
return {
|
|
122
|
+
pass: upOk && downOk,
|
|
123
|
+
tally: up ? up.tally : null,
|
|
124
|
+
note: !upOk ? 'wrong result with X answering' : !downOk ? 'wrong result with X down' : 'checked, unverified, and unknown all land right',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Count every request so the stranger case can prove it never reached the server.
|
|
129
|
+
function startCountingServer() {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const state = { hits: 0 };
|
|
132
|
+
const server = http.createServer((req, res) => {
|
|
133
|
+
state.hits += 1;
|
|
134
|
+
res.writeHead(401, { 'content-type': 'application/json' });
|
|
135
|
+
res.end('{"detail":"bench server"}');
|
|
136
|
+
});
|
|
137
|
+
server.on('error', reject);
|
|
138
|
+
server.listen(0, '127.0.0.1', () => {
|
|
139
|
+
state.url = `http://127.0.0.1:${server.address().port}`;
|
|
140
|
+
state.close = () => new Promise((done) => server.close(() => done()));
|
|
141
|
+
resolve(state);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function runAtris(args, { env, cwd, timeoutMs }) {
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const started = Date.now();
|
|
149
|
+
const child = spawn(process.execPath, [ATRIS, ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
150
|
+
let stdout = '';
|
|
151
|
+
let stderr = '';
|
|
152
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
153
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
154
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
|
|
155
|
+
child.on('close', (status) => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
resolve({ status, stdout, stderr, seconds: Number(((Date.now() - started) / 1000).toFixed(1)) });
|
|
158
|
+
});
|
|
159
|
+
child.on('error', (err) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
resolve({ status: null, stdout, stderr: `${stderr}${err.message}`, seconds: Number(((Date.now() - started) / 1000).toFixed(1)) });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Run x-search as a stranger would: a bare HOME with no login, and the backend
|
|
167
|
+
// pointed at a local server that counts requests.
|
|
168
|
+
async function runStranger(args) {
|
|
169
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'xsearch-stranger-'));
|
|
170
|
+
const server = await startCountingServer();
|
|
171
|
+
try {
|
|
172
|
+
const run = await runAtris(args, {
|
|
173
|
+
cwd: tmp,
|
|
174
|
+
env: {
|
|
175
|
+
HOME: tmp,
|
|
176
|
+
PATH: `${path.dirname(process.execPath)}:/usr/bin:/bin`,
|
|
177
|
+
TMPDIR: tmp,
|
|
178
|
+
ATRIS_API_URL: `${server.url}/api`,
|
|
179
|
+
ATRIS_BACKEND_URL: server.url,
|
|
180
|
+
ATRIS_APP_URL: server.url,
|
|
181
|
+
CI: '1',
|
|
182
|
+
},
|
|
183
|
+
timeoutMs: STRANGER_TIMEOUT_MS,
|
|
184
|
+
});
|
|
185
|
+
return { ...run, hits: server.hits };
|
|
186
|
+
} finally {
|
|
187
|
+
await server.close();
|
|
188
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function lastLines(text, n = 3) {
|
|
193
|
+
return String(text || '').trim().split('\n').filter(Boolean).slice(-n).map((line) => ` ${line}`).join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function tallyText(t) {
|
|
197
|
+
return t ? `posts=${postCount(t)} checked=${t.checked} unverified=${t.unverified} unknown=${t.unknown}` : 'posts=0';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Keep what a failed paid search printed, so the miss can be read later
|
|
201
|
+
// without paying for another search.
|
|
202
|
+
function saveFailedOutput(dir, name, ts, text) {
|
|
203
|
+
try {
|
|
204
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
205
|
+
const file = path.join(dir, `xsearch-${name}-${ts.replace(/[:.]/g, '-')}.txt`);
|
|
206
|
+
fs.writeFileSync(file, text);
|
|
207
|
+
return file;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function runCase(c, outDir) {
|
|
214
|
+
const ts = new Date().toISOString();
|
|
215
|
+
if (c.kind === 'offline') {
|
|
216
|
+
const started = Date.now();
|
|
217
|
+
const verdict = await runOfflineCheck();
|
|
218
|
+
const seconds = Number(((Date.now() - started) / 1000).toFixed(1));
|
|
219
|
+
const t = verdict.tally;
|
|
220
|
+
return {
|
|
221
|
+
pass: verdict.pass,
|
|
222
|
+
line: `${c.name} ${verdict.pass ? 'pass' : 'fail'} ${seconds}s (${verdict.note}) ${tallyText(t)}`,
|
|
223
|
+
row: {
|
|
224
|
+
ts, case: c.name, seconds, pass: verdict.pass, exit: null,
|
|
225
|
+
posts: postCount(t),
|
|
226
|
+
checked: t ? t.checked : 0, unverified: t ? t.unverified : 0, unknown: t ? t.unknown : 0,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (c.kind === 'stranger') {
|
|
232
|
+
const run = await runStranger(c.args);
|
|
233
|
+
const verdict = judgeStranger({ status: run.status, text: `${run.stdout}\n${run.stderr}`, hits: run.hits });
|
|
234
|
+
return {
|
|
235
|
+
pass: verdict.pass,
|
|
236
|
+
line: `${c.name} ${verdict.pass ? 'pass' : 'fail'} ${run.seconds}s exit=${run.status === null ? 'none' : run.status} (${verdict.note})`,
|
|
237
|
+
detail: verdict.pass ? '' : lastLines(`${run.stdout}\n${run.stderr}`),
|
|
238
|
+
row: { ts, case: c.name, seconds: run.seconds, pass: verdict.pass, exit: run.status, posts: 0, checked: 0, unverified: 0, unknown: 0, hits: run.hits },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const run = await runAtris(c.args, { cwd: os.tmpdir(), env: process.env, timeoutMs: PAID_TIMEOUT_MS });
|
|
243
|
+
const tally = parseTally(run.stdout);
|
|
244
|
+
const verdict = judgePaid({ status: run.status, tally, minPosts: c.minPosts });
|
|
245
|
+
const saved = verdict.pass ? null : saveFailedOutput(outDir, c.name, ts, `${run.stdout}\n${run.stderr}`);
|
|
246
|
+
return {
|
|
247
|
+
pass: verdict.pass,
|
|
248
|
+
line: `${c.name} ${verdict.pass ? 'pass' : 'fail'} ${run.seconds}s exit=${run.status === null ? 'none' : run.status} (${verdict.note}) ${tallyText(tally)}`,
|
|
249
|
+
detail: verdict.pass ? '' : [lastLines(`${run.stdout}\n${run.stderr}`), saved ? ` full output: ${saved}` : null].filter(Boolean).join('\n'),
|
|
250
|
+
row: {
|
|
251
|
+
ts, case: c.name, seconds: run.seconds, pass: verdict.pass, exit: run.status,
|
|
252
|
+
posts: verdict.posts,
|
|
253
|
+
checked: tally ? tally.checked : 0, unverified: tally ? tally.unverified : 0, unknown: tally ? tally.unknown : 0,
|
|
254
|
+
...(saved ? { output: saved } : {}),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function summary(results, seconds) {
|
|
260
|
+
const passed = results.filter((r) => r.pass).length;
|
|
261
|
+
return `bench: ${passed}/${results.length} passed in ${seconds}s`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function appendRow(dir, row) {
|
|
265
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
266
|
+
fs.appendFileSync(path.join(dir, 'xsearch.jsonl'), `${JSON.stringify(row)}\n`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function main() {
|
|
270
|
+
let cases;
|
|
271
|
+
try {
|
|
272
|
+
cases = selectCases(parseArgs(process.argv.slice(2)));
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error(err.message);
|
|
275
|
+
console.error('usage: xsearch-bench [--quick|--free] [--case <name>]');
|
|
276
|
+
process.exit(2);
|
|
277
|
+
}
|
|
278
|
+
const outDir = process.env.XSEARCH_OUT_DIR || path.join(os.homedir(), '.atris', 'benchmarks');
|
|
279
|
+
const started = Date.now();
|
|
280
|
+
const results = [];
|
|
281
|
+
for (const c of cases) {
|
|
282
|
+
const result = await runCase(c, outDir);
|
|
283
|
+
results.push(result);
|
|
284
|
+
console.log(result.line);
|
|
285
|
+
if (result.detail) console.log(result.detail);
|
|
286
|
+
appendRow(outDir, result.row);
|
|
287
|
+
}
|
|
288
|
+
console.log(summary(results, Math.round((Date.now() - started) / 1000)));
|
|
289
|
+
process.exit(results.some((r) => !r.pass) ? 1 : 0);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (require.main === module) main();
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
CASES,
|
|
296
|
+
FIXTURE,
|
|
297
|
+
parseArgs,
|
|
298
|
+
selectCases,
|
|
299
|
+
parseTally,
|
|
300
|
+
postCount,
|
|
301
|
+
judgePaid,
|
|
302
|
+
judgeStranger,
|
|
303
|
+
fixtureFetch,
|
|
304
|
+
runOfflineCheck,
|
|
305
|
+
saveFailedOutput,
|
|
306
|
+
summary,
|
|
307
|
+
};
|
package/scripts/det/ytnotes
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# No captions: if mlx_whisper is installed (uv tool install mlx-whisper, Apple
|
|
5
5
|
# silicon), the audio is transcribed on this machine instead. Otherwise the
|
|
6
6
|
# script prints the paid command. ATRIS_YTNOTES_LOCAL_TRANSCRIBE=0 turns it off.
|
|
7
|
-
# Engines: auto (default:
|
|
8
|
-
# Flash through agy, then Haiku,
|
|
7
|
+
# Engines: auto (default: the team's notes writer from the roster, then the
|
|
8
|
+
# fastest installed writer that answers: Gemini Flash through agy, then Haiku,
|
|
9
|
+
# Codex, Cursor),
|
|
9
10
|
# gemini, haiku, atris-fast, grok, codex, cursor. ATRIS_YTNOTES_ENGINE sets the default.
|
|
10
11
|
# Outputs: $WORK/yt_${ID}.clean.txt (transcript), $WORK/yt_${ID}.md (notes).
|
|
11
12
|
set -euo pipefail
|
|
@@ -372,40 +373,85 @@ run_grok() {
|
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
run_haiku() {
|
|
375
|
-
run_with_timeout 240 claude -p "$PROMPT" --model claude-haiku-4-5
|
|
376
|
+
run_with_timeout 240 claude -p "$PROMPT" --model "${1:-claude-haiku-4-5}"
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
# Gemini Flash through the Antigravity CLI (agy). On an 85-minute transcript it
|
|
379
380
|
# writes notes in about 8s; Haiku takes 30-40s. The old gemini CLI is retired.
|
|
381
|
+
# Low effort is the floor for notes: high effort is slower and not sharper here.
|
|
380
382
|
run_gemini() {
|
|
381
383
|
command -v agy >/dev/null 2>&1 || { echo "agy is not installed" >&2; return 127; }
|
|
382
|
-
run_with_timeout 120 agy -p "$PROMPT" --model "${ATRIS_YTNOTES_GEMINI_MODEL:-gemini-3.8-flash}" \
|
|
383
|
-
--effort low --print-timeout 110s </dev/null
|
|
384
|
+
run_with_timeout 120 agy -p "$PROMPT" --model "${ATRIS_YTNOTES_GEMINI_MODEL:-${1:-gemini-3.8-flash}}" \
|
|
385
|
+
--effort "${2:-low}" --print-timeout 110s </dev/null
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
# Fastest installed writer that answers: Gemini, Haiku, Codex, Cursor.
|
|
389
|
+
# Entries are cmd|model|effort|label; an empty model or effort means the default.
|
|
387
390
|
# Buffered so a failed or empty answer never reaches the notes file.
|
|
388
|
-
AUTO_WRITERS="agy
|
|
391
|
+
AUTO_WRITERS="agy|||Gemini claude|||Haiku codex|||Codex cursor-agent|||Cursor"
|
|
392
|
+
|
|
393
|
+
# The team's "notes" writer, from `atris engine resolve notes --json`, run from
|
|
394
|
+
# the caller's project with this same package. It only reorders: any failure,
|
|
395
|
+
# a slow answer (2s), or a writer that is not installed falls back to the order
|
|
396
|
+
# above. ATRIS_YTNOTES_ROSTER=0 skips it.
|
|
397
|
+
resolve_roster_writers() {
|
|
398
|
+
[ "${ATRIS_YTNOTES_ROSTER:-1}" = "0" ] && return 0
|
|
399
|
+
local json=""
|
|
400
|
+
if [ -n "${ATRIS_YTNOTES_RESOLVE_CMD:-}" ]; then
|
|
401
|
+
json=$(cd "$CALLER_CWD" && run_with_timeout 2 sh -c "$ATRIS_YTNOTES_RESOLVE_CMD" 2>/dev/null) || return 0
|
|
402
|
+
else
|
|
403
|
+
[ -f "$SCRIPT_DIR/../../bin/atris.js" ] || return 0
|
|
404
|
+
json=$(cd "$CALLER_CWD" && ATRIS_AUTO_UPDATE=off run_with_timeout 2 node "$SCRIPT_DIR/../../bin/atris.js" engine resolve notes --json 2>/dev/null) || return 0
|
|
405
|
+
fi
|
|
406
|
+
printf '%s' "$json" | node -e '
|
|
407
|
+
const s = require("fs").readFileSync(0, "utf8");
|
|
408
|
+
const d = JSON.parse(s.slice(s.indexOf("{"), s.lastIndexOf("}") + 1));
|
|
409
|
+
const cmds = { agy: "agy", gemini: "agy", haiku: "claude", claude: "claude", codex: "codex", cursor: "cursor-agent", "cursor-agent": "cursor-agent" };
|
|
410
|
+
const clean = (v) => (v == null ? "" : String(v).replace(/[^A-Za-z0-9._:\/-]/g, ""));
|
|
411
|
+
const label = (cmd, model) => ({ agy: "Gemini", codex: "Codex", "cursor-agent": "Cursor" }[cmd]
|
|
412
|
+
|| (/haiku/i.test(model || "haiku") ? "Haiku" : "Claude"));
|
|
413
|
+
const out = [];
|
|
414
|
+
for (const w of [d, ...(Array.isArray(d.backup) ? d.backup : [])]) {
|
|
415
|
+
const cmd = cmds[String((w && w.engine) || "").toLowerCase()];
|
|
416
|
+
if (!cmd) continue;
|
|
417
|
+
const model = clean(w.model);
|
|
418
|
+
out.push([cmd, model, clean(w.effort), label(cmd, model)].join("|"));
|
|
419
|
+
}
|
|
420
|
+
process.stdout.write(out.join(" "));
|
|
421
|
+
' 2>/dev/null || true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# Roster writers first, then the rest of the built-in order, one entry per cmd.
|
|
425
|
+
writer_list() {
|
|
426
|
+
local entry cmd seen=" " list=""
|
|
427
|
+
for entry in $(resolve_roster_writers) $AUTO_WRITERS; do
|
|
428
|
+
cmd="${entry%%|*}"
|
|
429
|
+
case "$seen" in *" $cmd "*) continue ;; esac
|
|
430
|
+
seen="$seen$cmd "
|
|
431
|
+
list="$list $entry"
|
|
432
|
+
done
|
|
433
|
+
printf '%s' "$list"
|
|
434
|
+
}
|
|
389
435
|
|
|
390
436
|
run_writer() {
|
|
437
|
+
local model="${2:-}"
|
|
391
438
|
case "$1" in
|
|
392
|
-
agy) run_gemini ;;
|
|
393
|
-
claude) run_haiku ;;
|
|
394
|
-
codex) run_with_timeout 240 codex exec --sandbox read-only --skip-git-repo-check "$PROMPT" </dev/null ;;
|
|
395
|
-
cursor-agent) run_with_timeout 240 cursor-agent --trust -p "$PROMPT" ;;
|
|
439
|
+
agy) run_gemini "$model" "${3:-}" ;;
|
|
440
|
+
claude) run_haiku "$model" ;;
|
|
441
|
+
codex) run_with_timeout 240 codex exec ${model:+-m "$model"} --sandbox read-only --skip-git-repo-check "$PROMPT" </dev/null ;;
|
|
442
|
+
cursor-agent) run_with_timeout 240 cursor-agent --trust ${model:+--model "$model"} -p "$PROMPT" ;;
|
|
396
443
|
esac
|
|
397
444
|
}
|
|
398
445
|
|
|
399
446
|
run_auto() {
|
|
400
|
-
local entry cmd label out tried=""
|
|
401
|
-
for entry in $
|
|
402
|
-
cmd
|
|
403
|
-
label="${entry#*:}"
|
|
447
|
+
local entry cmd model effort label out tried=""
|
|
448
|
+
for entry in $(writer_list); do
|
|
449
|
+
IFS='|' read -r cmd model effort label <<< "$entry"
|
|
404
450
|
command -v "$cmd" >/dev/null 2>&1 || continue
|
|
405
451
|
if [ -n "$tried" ]; then
|
|
406
452
|
echo "$tried did not answer; writing the notes with $label instead." >&2
|
|
407
453
|
fi
|
|
408
|
-
if out=$(run_writer "$cmd" 2>>"$WORK/yt_${ID}.engine.err") && printf '%s' "$out" | grep -q '[[:alnum:]]'; then
|
|
454
|
+
if out=$(run_writer "$cmd" "$model" "$effort" 2>>"$WORK/yt_${ID}.engine.err") && printf '%s' "$out" | grep -q '[[:alnum:]]'; then
|
|
409
455
|
printf '%s\n' "$out"
|
|
410
456
|
echo "notes by $label" >&2
|
|
411
457
|
return 0
|
|
@@ -12,6 +12,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const os = require('node:os');
|
|
14
14
|
const path = require('node:path');
|
|
15
|
+
const { stateHome } = require('../../lib/state-home');
|
|
15
16
|
const { evaluate, appendRow } = require('./ytrail-eval');
|
|
16
17
|
|
|
17
18
|
const YTNOTES = path.join(__dirname, 'ytnotes');
|
|
@@ -186,7 +187,7 @@ function main() {
|
|
|
186
187
|
console.error('usage: ytrail-bench [--quick] [--case <name>]');
|
|
187
188
|
process.exit(2);
|
|
188
189
|
}
|
|
189
|
-
const outDir = process.env.YTRAIL_OUT_DIR || path.join(
|
|
190
|
+
const outDir = process.env.YTRAIL_OUT_DIR || path.join(stateHome(), '.atris', 'benchmarks');
|
|
190
191
|
const started = Date.now();
|
|
191
192
|
const results = [];
|
|
192
193
|
for (const c of cases) {
|
package/utils/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const { exec } = require('child_process');
|
|
5
6
|
const readline = require('readline');
|
|
@@ -171,8 +172,7 @@ function restrictToOwner(filePath) {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
function getAtrisDir() {
|
|
174
|
-
|
|
175
|
-
return mkPrivateDir(path.join(homeDir, '.atris'));
|
|
175
|
+
return mkPrivateDir(path.join(stateHome(), '.atris'));
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
function getCredentialsPath() {
|
|
@@ -182,7 +182,7 @@ function getCredentialsPath() {
|
|
|
182
182
|
function getPlacedAgentTokenPath() {
|
|
183
183
|
const override = process.env.ATRIS_AGENT_TOKEN_FILE;
|
|
184
184
|
if (override && override.trim()) return override.trim();
|
|
185
|
-
return path.join(
|
|
185
|
+
return path.join(stateHome(), '.atris', 'agent-token.json');
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function expiryTimeMs(value) {
|
package/utils/update-check.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const
|
|
4
|
+
const { stateHome } = require('../lib/state-home');
|
|
5
5
|
const { spawn, spawnSync } = require('child_process');
|
|
6
6
|
|
|
7
7
|
const PACKAGE_NAME = 'atris';
|
|
8
8
|
const NPM_SELF_UPDATE_COMMAND = `npm install -g ${PACKAGE_NAME}@latest`;
|
|
9
9
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
-
const ATRIS_DIR = path.join(
|
|
10
|
+
const ATRIS_DIR = path.join(stateHome(), '.atris');
|
|
11
11
|
const CACHE_FILE = path.join(ATRIS_DIR, '.update-check');
|
|
12
12
|
|
|
13
13
|
/**
|