atris 3.16.0 → 3.17.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 +33 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +446 -43
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +466 -20
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +574 -0
- package/commands/console.js +8 -3
- package/commands/deck.js +135 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +444 -0
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +233 -0
- package/commands/run.js +615 -22
- package/commands/skill.js +6 -2
- package/commands/slop.js +173 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +458 -43
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/slides-deck.js +236 -0
- package/lib/state-detection.js +40 -3
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
package/commands/now.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { hasRenderedSections, isOpenSection } = require('../lib/todo-sections');
|
|
3
4
|
|
|
4
5
|
const NOW_PATH = path.join('atris', 'now.md');
|
|
5
6
|
const TASK_EPISODES_PATH = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -71,7 +72,8 @@ function countMatches(filePath, pattern) {
|
|
|
71
72
|
function countOpenTodoItems(filePath) {
|
|
72
73
|
if (!fs.existsSync(filePath)) return 0;
|
|
73
74
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
74
|
-
|
|
75
|
+
// Emoji-decorated headings ("## In Progress 🔄") handled by lib/todo-sections.
|
|
76
|
+
const rendered = hasRenderedSections(content);
|
|
75
77
|
let section = null;
|
|
76
78
|
let count = 0;
|
|
77
79
|
|
|
@@ -83,7 +85,7 @@ function countOpenTodoItems(filePath) {
|
|
|
83
85
|
}
|
|
84
86
|
const isTaskBullet = /^-\s+(?:\[[ ]\]\s+)?\*\*.+?\*\*/.test(line);
|
|
85
87
|
if (!isTaskBullet) continue;
|
|
86
|
-
if (!
|
|
88
|
+
if (!rendered || isOpenSection(section)) {
|
|
87
89
|
count += 1;
|
|
88
90
|
}
|
|
89
91
|
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// atris probe — chat-lane probe (TRR-22): one REAL /atris2/turn over the full
|
|
2
|
+
// tool relay, exactly as the surfaces run it. Port of terrace's
|
|
3
|
+
// atris/bin/relay-probe and atris/bin/calendar-probe — keep the op tables in
|
|
4
|
+
// lockstep across iOS (votd/GMModeAPI.swift Atris2ToolRelay), web
|
|
5
|
+
// (atrisos-web orbToolRelay.ts), Obelisk (atris2LocalFileOp.cjs), and the
|
|
6
|
+
// terrace probes.
|
|
7
|
+
//
|
|
8
|
+
// Contract: the turn body sends `local_executor: true` plus a `workspace_path`
|
|
9
|
+
// LABEL (/workspace/personal or /workspace/business-{id}). The backend relays
|
|
10
|
+
// `local_file_op` / `local_atris_cli_op` calls down the SSE stream; we execute
|
|
11
|
+
// each against the computer via POST /ai-computer/terminal and POST the result
|
|
12
|
+
// to /atris2/turn/tool-result. Label-absolute paths are rewritten
|
|
13
|
+
// root-relative (the runner's /bash cwd is the workspace ROOT).
|
|
14
|
+
//
|
|
15
|
+
// PASS = >=1 relayed op (with --calendar: >=1 calendar cli op) AND a non-empty
|
|
16
|
+
// final answer with no dead-end marker; otherwise prints the marker in the
|
|
17
|
+
// receipt line and exits 1 — a FAIL naming the dead-end is the instrument
|
|
18
|
+
// working. The receipt line is journal-ready.
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// atris probe # personal lane, file question
|
|
22
|
+
// atris probe --calendar # calendar question (cli-op lane)
|
|
23
|
+
// atris probe --business <id> [--model atris:pro]
|
|
24
|
+
// atris probe --member-slug relay # turn runs AS the member
|
|
25
|
+
// atris probe --message "..." # custom question
|
|
26
|
+
|
|
27
|
+
const https = require('https');
|
|
28
|
+
const http = require('http');
|
|
29
|
+
const { getApiBaseUrl } = require('../utils/api');
|
|
30
|
+
const { loadCredentials } = require('../utils/auth');
|
|
31
|
+
|
|
32
|
+
// G2's honest blocked message (tool_policy_bench.MAX_TURNS_EXHAUSTED_MESSAGE)
|
|
33
|
+
// starts with this — an answer that is only this marker is a dead-end.
|
|
34
|
+
const MAX_TURNS_MARKER = 'i ran out of tool budget for this turn';
|
|
35
|
+
|
|
36
|
+
function workspaceLabel(businessId) {
|
|
37
|
+
return businessId ? `/workspace/business-${businessId}` : '/workspace/personal';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shq(value) {
|
|
41
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Port of Atris2ToolRelay path normalization (lockstep) ---
|
|
45
|
+
|
|
46
|
+
function normalizePath(raw, label) {
|
|
47
|
+
const l = label.replace(/\/$/, '');
|
|
48
|
+
if (!l) return raw;
|
|
49
|
+
if (raw === l || raw === l + '/') return '.';
|
|
50
|
+
if (raw.startsWith(l + '/')) return raw.slice(l.length + 1) || '.';
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeBash(cmd, label) {
|
|
55
|
+
const l = label.replace(/\/$/, '');
|
|
56
|
+
if (!l) return cmd;
|
|
57
|
+
return cmd.split(l + '/').join('').split(l).join('.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// No write/edit file ops, and the `..` guard only covers args.path. The
|
|
61
|
+
// `bash` op still executes whatever command the model sends, verbatim, on
|
|
62
|
+
// the remote ai-computer — that is the production relay contract and the
|
|
63
|
+
// table must stay in lockstep with it, so this probe is NOT read-only.
|
|
64
|
+
function fileOpCommand(args, label) {
|
|
65
|
+
const op = String(args.type || '').toLowerCase();
|
|
66
|
+
const raw = normalizePath(String(args.path || '') || '.', label);
|
|
67
|
+
if (raw.split('/').includes('..')) return null;
|
|
68
|
+
const p = shq(raw);
|
|
69
|
+
if (op === 'bash') return `( ${normalizeBash(String(args.command || '') || 'true', label)} )`;
|
|
70
|
+
if (op === 'list') return `find ${p} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' | head -200`;
|
|
71
|
+
if (op === 'search') {
|
|
72
|
+
const q = shq(String(args.query || args.pattern || ''));
|
|
73
|
+
return `grep -rn -m 50 ${q} ${p} 2>/dev/null | head -50`;
|
|
74
|
+
}
|
|
75
|
+
if (op === 'read') return `{ [ -d ${p} ] && ls -p ${p} | head -200 || head -c 12000 ${p}; }`;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Port of Atris2ToolRelay.atrisCLICommand (GMModeAPI.swift) — op → `atris …`.
|
|
80
|
+
const SIMPLE_CLI_OPS = {
|
|
81
|
+
status: ['atris.md'],
|
|
82
|
+
integrations_status: ['integrations'],
|
|
83
|
+
calendar_today: ['calendar', 'today'],
|
|
84
|
+
calendar_yesterday: ['calendar', 'yesterday'],
|
|
85
|
+
calendar_week: ['calendar', 'week'],
|
|
86
|
+
gmail_inbox: ['gmail', 'inbox'],
|
|
87
|
+
slack_channels: ['slack', 'channels'],
|
|
88
|
+
slack_dms: ['slack', 'dms'],
|
|
89
|
+
task_status: ['task', 'status', '--json'],
|
|
90
|
+
task_list: ['task', 'list', '--json'],
|
|
91
|
+
skill_list: ['skill', 'list'],
|
|
92
|
+
member_list: ['member', 'list'],
|
|
93
|
+
mission_status: ['mission', 'status', '--json'],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function atrisCliCommand(args) {
|
|
97
|
+
const op = String(args.type || '').toLowerCase();
|
|
98
|
+
let argv = SIMPLE_CLI_OPS[op] || null;
|
|
99
|
+
if (!argv) {
|
|
100
|
+
if (op === 'task_show') {
|
|
101
|
+
const taskId = String(args.task_id || '');
|
|
102
|
+
if (taskId) argv = ['task', 'show', taskId, '--json'];
|
|
103
|
+
} else if (op === 'member_status') {
|
|
104
|
+
const member = String(args.member || '');
|
|
105
|
+
if (member) argv = ['member', 'status', member, '--json'];
|
|
106
|
+
} else if (op === 'calendar_date') {
|
|
107
|
+
const date = String(args.date || '');
|
|
108
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) argv = ['calendar', 'date', date];
|
|
109
|
+
} else if (op === 'slack_messages') {
|
|
110
|
+
const channel = String(args.channel || '');
|
|
111
|
+
if (channel) argv = ['slack', 'messages', channel, '--limit', '20'];
|
|
112
|
+
} else if (op === 'slack_search') {
|
|
113
|
+
const query = String(args.query || '');
|
|
114
|
+
if (query) argv = ['slack', 'search', query, '--limit', '20'];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!argv) return null;
|
|
118
|
+
return 'atris ' + argv.map(shq).join(' ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function atrisCliResult(command, term) {
|
|
122
|
+
// a terminal response without a numeric exit_code is a broken endpoint,
|
|
123
|
+
// not a success — never let it masquerade as ok with empty stdout
|
|
124
|
+
const code = typeof term.exit_code === 'number' ? term.exit_code : -1;
|
|
125
|
+
const out = {
|
|
126
|
+
schema: 'atris.local_cli_result.v1',
|
|
127
|
+
status: code === 0 ? 'ok' : 'error',
|
|
128
|
+
command,
|
|
129
|
+
stdout: String(term.stdout || '').slice(0, 12000),
|
|
130
|
+
exit_code: code,
|
|
131
|
+
};
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
out.error = typeof term.exit_code === 'number'
|
|
134
|
+
? String(term.stderr || term.stdout || 'command failed').slice(0, 2000)
|
|
135
|
+
: 'terminal endpoint returned no exit_code';
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- HTTP helpers (Bearer auth against the prod API, like the terrace probes) ---
|
|
141
|
+
|
|
142
|
+
function postJson(urlString, token, payload, timeoutMs) {
|
|
143
|
+
const url = new URL(urlString);
|
|
144
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const body = JSON.stringify(payload);
|
|
147
|
+
const req = transport.request({
|
|
148
|
+
hostname: url.hostname,
|
|
149
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
150
|
+
path: url.pathname,
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
'Content-Length': Buffer.byteLength(body),
|
|
155
|
+
Authorization: `Bearer ${token}`,
|
|
156
|
+
},
|
|
157
|
+
timeout: timeoutMs,
|
|
158
|
+
}, (res) => {
|
|
159
|
+
let data = '';
|
|
160
|
+
res.on('data', (c) => data += c);
|
|
161
|
+
res.on('end', () => {
|
|
162
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
163
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!data) { resolve({}); return; }
|
|
167
|
+
try { resolve(JSON.parse(data)); } catch (e) {
|
|
168
|
+
reject(new Error(`invalid JSON from ${url.pathname}: ${data.slice(0, 120)}`));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
req.on('timeout', () => { req.destroy(new Error('request timeout')); });
|
|
173
|
+
req.on('error', reject);
|
|
174
|
+
req.write(body);
|
|
175
|
+
req.end();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runTerminal(base, token, command, businessId) {
|
|
180
|
+
const body = { command, timeout: 60 };
|
|
181
|
+
if (businessId) body.business_id = businessId;
|
|
182
|
+
return postJson(`${base}/ai-computer/terminal`, token, body, 80000);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseArgs(argv) {
|
|
186
|
+
const a = { business: null, model: 'atris:fast', memberId: null, memberSlug: null, calendar: false, message: null };
|
|
187
|
+
for (let i = 0; i < argv.length; i++) {
|
|
188
|
+
const flag = argv[i];
|
|
189
|
+
if (flag === '--business') a.business = argv[++i] || null;
|
|
190
|
+
else if (flag === '--model') a.model = argv[++i] || a.model;
|
|
191
|
+
else if (flag === '--member-id') a.memberId = argv[++i] || null;
|
|
192
|
+
else if (flag === '--member-slug') a.memberSlug = argv[++i] || null;
|
|
193
|
+
else if (flag === '--calendar') a.calendar = true;
|
|
194
|
+
else if (flag === '--message') a.message = argv[++i] || null;
|
|
195
|
+
else if (flag === '--help' || flag === '-h') {
|
|
196
|
+
console.log('Usage: atris probe [--calendar] [--business <id>] [--model atris:fast] [--member-slug <slug>] [--member-id <id>] [--message "..."]');
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return a;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Core /atris2/turn client: one streamed turn over the full local tool relay.
|
|
204
|
+
// Shared by `atris probe` (the instrument) and `atris mission run --runner atris2`
|
|
205
|
+
// (the worker). Transport-level outcome only — callers apply their own
|
|
206
|
+
// pass/fail policy on top of the returned fields.
|
|
207
|
+
async function runAtris2Turn(opts = {}) {
|
|
208
|
+
const {
|
|
209
|
+
prompt,
|
|
210
|
+
model = 'atris:fast',
|
|
211
|
+
business = null,
|
|
212
|
+
memberId = null,
|
|
213
|
+
memberSlug = null,
|
|
214
|
+
maxTurns = 8,
|
|
215
|
+
idleMs = 180000,
|
|
216
|
+
connectTimeoutMs = 60000,
|
|
217
|
+
signal = null,
|
|
218
|
+
} = opts;
|
|
219
|
+
const t0 = Date.now();
|
|
220
|
+
const out = { ok: false, text: '', engine: null, tools_run: 0, cli_ops: [], unsupported: [], error: null, duration_ms: 0 };
|
|
221
|
+
const creds = loadCredentials();
|
|
222
|
+
if (!creds || !creds.token) {
|
|
223
|
+
out.error = 'not-logged-in';
|
|
224
|
+
out.duration_ms = Date.now() - t0;
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
const token = creds.token;
|
|
228
|
+
const base = getApiBaseUrl();
|
|
229
|
+
const label = workspaceLabel(business);
|
|
230
|
+
|
|
231
|
+
const body = {
|
|
232
|
+
message: prompt, model, max_turns: maxTurns,
|
|
233
|
+
verify_command: 'true', local_executor: true, workspace_path: label,
|
|
234
|
+
};
|
|
235
|
+
if (memberId) body.member_id = memberId;
|
|
236
|
+
if (memberSlug) body.member_slug = memberSlug;
|
|
237
|
+
|
|
238
|
+
let toolsRun = 0;
|
|
239
|
+
let resultText = '';
|
|
240
|
+
let err = null;
|
|
241
|
+
let engine = null;
|
|
242
|
+
const cliOps = [];
|
|
243
|
+
const unsupported = [];
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await new Promise((resolve, reject) => {
|
|
247
|
+
const url = new URL(`${base}/atris2/turn`);
|
|
248
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
249
|
+
const postData = JSON.stringify(body);
|
|
250
|
+
const req = transport.request({
|
|
251
|
+
hostname: url.hostname,
|
|
252
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
253
|
+
path: url.pathname,
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/json',
|
|
257
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
258
|
+
Accept: 'text/event-stream',
|
|
259
|
+
Authorization: `Bearer ${token}`,
|
|
260
|
+
},
|
|
261
|
+
// connect/first-byte guard; cleared once headers arrive so the
|
|
262
|
+
// idle timer below is the only judge of a flowing stream
|
|
263
|
+
timeout: connectTimeoutMs,
|
|
264
|
+
}, (res) => {
|
|
265
|
+
req.setTimeout(0);
|
|
266
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
267
|
+
let data = '';
|
|
268
|
+
res.on('data', (c) => data += c);
|
|
269
|
+
res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
let buffer = '';
|
|
273
|
+
let idleTimer = null;
|
|
274
|
+
const resetIdle = () => {
|
|
275
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
276
|
+
idleTimer = setTimeout(() => { req.destroy(); reject(new Error(`stream stalled: no events for ${idleMs / 1000}s`)); }, idleMs);
|
|
277
|
+
};
|
|
278
|
+
resetIdle();
|
|
279
|
+
|
|
280
|
+
// Relayed tool calls run sequentially: the backend awaits each result
|
|
281
|
+
// before continuing the loop, so a promise chain preserves order.
|
|
282
|
+
let toolChain = Promise.resolve();
|
|
283
|
+
|
|
284
|
+
const handleEvent = (ev) => {
|
|
285
|
+
if (!ev || typeof ev !== 'object') return;
|
|
286
|
+
const et = ev.type;
|
|
287
|
+
if (et === 'system_init') {
|
|
288
|
+
engine = String(ev.model || '') || null;
|
|
289
|
+
} else if (et === 'tool_call_request') {
|
|
290
|
+
const name = String(ev.name || '');
|
|
291
|
+
const args = ev.args || ev.arguments || {};
|
|
292
|
+
const op = String(args.type || '').toLowerCase();
|
|
293
|
+
toolsRun += 1;
|
|
294
|
+
toolChain = toolChain.then(async () => {
|
|
295
|
+
let out;
|
|
296
|
+
if (name === 'local_file_op') {
|
|
297
|
+
const cmd = fileOpCommand(args, label);
|
|
298
|
+
if (cmd === null) {
|
|
299
|
+
out = { status: 'error', error: 'unsupported op or unsafe path' };
|
|
300
|
+
unsupported.push(`file_op:${op}`);
|
|
301
|
+
} else {
|
|
302
|
+
const term = await runTerminal(base, token, cmd, business);
|
|
303
|
+
const ok = term.exit_code === 0;
|
|
304
|
+
out = ok
|
|
305
|
+
? { status: 'ok', stdout: String(term.stdout || '').slice(0, 12000) }
|
|
306
|
+
: {
|
|
307
|
+
status: 'error',
|
|
308
|
+
error: typeof term.exit_code === 'number'
|
|
309
|
+
? String(term.stderr || 'command failed').slice(0, 2000)
|
|
310
|
+
: 'terminal endpoint returned no exit_code',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
} else if (name === 'local_atris_cli_op') {
|
|
314
|
+
const cmd = atrisCliCommand(args);
|
|
315
|
+
if (cmd === null) {
|
|
316
|
+
out = { status: 'error', error: `unsupported atris cli op: ${op || '?'}` };
|
|
317
|
+
unsupported.push(`cli_op:${op || '?'}`);
|
|
318
|
+
} else {
|
|
319
|
+
const term = await runTerminal(base, token, cmd, business);
|
|
320
|
+
out = atrisCliResult(cmd, term);
|
|
321
|
+
cliOps.push(op || '?');
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
out = { status: 'error', error: `unsupported relayed tool: ${name || '?'}` };
|
|
325
|
+
unsupported.push(`tool:${name || '?'}`);
|
|
326
|
+
}
|
|
327
|
+
await postJson(`${base}/atris2/turn/tool-result`, token,
|
|
328
|
+
{ call_id: ev.call_id || ev.id, result: out }, 30000);
|
|
329
|
+
resetIdle();
|
|
330
|
+
}).catch((e) => reject(e));
|
|
331
|
+
} else if (et === 'result') {
|
|
332
|
+
resultText = String(ev.result || '');
|
|
333
|
+
} else if (et === 'error') {
|
|
334
|
+
err = String(ev.error || 'turn error');
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
res.setEncoding('utf8');
|
|
339
|
+
res.on('data', (chunk) => {
|
|
340
|
+
resetIdle();
|
|
341
|
+
buffer += chunk;
|
|
342
|
+
let nl;
|
|
343
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
344
|
+
const line = buffer.slice(0, nl).replace(/\r$/, '');
|
|
345
|
+
buffer = buffer.slice(nl + 1);
|
|
346
|
+
if (!line.startsWith('data: ')) continue;
|
|
347
|
+
try { handleEvent(JSON.parse(line.slice(6))); } catch (e) { /* malformed frame */ }
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
res.on('end', () => {
|
|
351
|
+
toolChain.then(() => { if (idleTimer) clearTimeout(idleTimer); resolve(); });
|
|
352
|
+
});
|
|
353
|
+
res.on('error', (e) => { if (idleTimer) clearTimeout(idleTimer); reject(e); });
|
|
354
|
+
});
|
|
355
|
+
req.on('timeout', () => { req.destroy(new Error(`no response headers within ${Math.round(connectTimeoutMs / 1000)}s`)); });
|
|
356
|
+
req.on('error', reject);
|
|
357
|
+
if (signal) signal.addEventListener('abort', () => { req.destroy(new Error('aborted')); }, { once: true });
|
|
358
|
+
req.write(postData);
|
|
359
|
+
req.end();
|
|
360
|
+
});
|
|
361
|
+
} catch (e) {
|
|
362
|
+
if (!err) err = `${e.name || 'Error'}: ${e.message || e}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
out.text = resultText;
|
|
366
|
+
out.engine = engine;
|
|
367
|
+
out.tools_run = toolsRun;
|
|
368
|
+
out.cli_ops = cliOps;
|
|
369
|
+
out.unsupported = unsupported;
|
|
370
|
+
out.error = err;
|
|
371
|
+
out.ok = err === null;
|
|
372
|
+
out.duration_ms = Date.now() - t0;
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function probeCommand(argv) {
|
|
377
|
+
const a = parseArgs(argv || []);
|
|
378
|
+
const creds = loadCredentials();
|
|
379
|
+
if (!creds || !creds.token) {
|
|
380
|
+
console.error('✗ Not logged in. Run: atris login');
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
const where = a.business ? `business ${String(a.business).slice(0, 8)}` : 'personal';
|
|
384
|
+
const member = a.memberSlug || a.memberId;
|
|
385
|
+
const prompt = a.message || (a.calendar
|
|
386
|
+
? "What's on my calendar today? Use your tools."
|
|
387
|
+
: 'Read the first 5 lines of any markdown file in this workspace and quote one real line from it. Use your file tools.');
|
|
388
|
+
|
|
389
|
+
const t0 = Date.now();
|
|
390
|
+
const turn = await runAtris2Turn({
|
|
391
|
+
prompt, model: a.model, business: a.business,
|
|
392
|
+
memberId: a.memberId, memberSlug: a.memberSlug, maxTurns: 8,
|
|
393
|
+
});
|
|
394
|
+
const toolsRun = turn.tools_run;
|
|
395
|
+
const cliCalendarOps = turn.cli_ops.filter((op) => String(op).startsWith('calendar')).length;
|
|
396
|
+
const resultText = turn.text;
|
|
397
|
+
const err = turn.error;
|
|
398
|
+
const engine = turn.engine;
|
|
399
|
+
const unsupported = turn.unsupported;
|
|
400
|
+
|
|
401
|
+
// Name the dead-end (first match wins); null = converged.
|
|
402
|
+
let deadEnd = null;
|
|
403
|
+
if (err) {
|
|
404
|
+
deadEnd = `error: ${String(err).slice(0, 120)}`;
|
|
405
|
+
} else if (a.calendar && cliCalendarOps === 0) {
|
|
406
|
+
deadEnd = 'no local_atris_cli_op calendar op relayed'
|
|
407
|
+
+ (unsupported.length ? ` (unsupported: ${unsupported.slice(0, 3).join(',')})` : '')
|
|
408
|
+
+ (toolsRun ? ` (tools=${toolsRun})` : ' (zero tool calls)');
|
|
409
|
+
} else if (!a.calendar && toolsRun === 0) {
|
|
410
|
+
deadEnd = 'zero relayed tool calls';
|
|
411
|
+
} else if (unsupported.length) {
|
|
412
|
+
deadEnd = `unsupported relayed call(s): ${unsupported.slice(0, 3).join(',')}`;
|
|
413
|
+
} else if (resultText.trim().length <= 20) {
|
|
414
|
+
deadEnd = 'empty/short final answer';
|
|
415
|
+
} else if (resultText.toLowerCase().includes(MAX_TURNS_MARKER)) {
|
|
416
|
+
deadEnd = 'max turns exhausted (honest G2 blocked message)';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const secs = Math.round((Date.now() - t0) / 100) / 10;
|
|
420
|
+
const ok = deadEnd === null;
|
|
421
|
+
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
422
|
+
const detail = ok
|
|
423
|
+
? `answer: ${resultText.slice(0, 80).replace(/\n/g, ' ')}`
|
|
424
|
+
: `dead-end: ${deadEnd}`;
|
|
425
|
+
const line = `- **atris-probe** \`${stamp}\` — ${where} · ${a.model}`
|
|
426
|
+
+ (member ? ` · member=${member}` : '')
|
|
427
|
+
+ (engine ? ` · engine=${engine}` : '')
|
|
428
|
+
+ ` · ${ok ? 'PASS' : 'FAIL'} · ${secs}s · tools=${toolsRun}`
|
|
429
|
+
+ (a.calendar ? ` cal_cli=${cliCalendarOps}` : '')
|
|
430
|
+
+ ` · ${detail}`;
|
|
431
|
+
console.log(line);
|
|
432
|
+
return ok ? 0 : 1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = {
|
|
436
|
+
probeCommand,
|
|
437
|
+
runAtris2Turn,
|
|
438
|
+
// exported for tests
|
|
439
|
+
normalizePath,
|
|
440
|
+
normalizeBash,
|
|
441
|
+
fileOpCommand,
|
|
442
|
+
atrisCliCommand,
|
|
443
|
+
atrisCliResult,
|
|
444
|
+
};
|