atris 3.15.57 → 3.16.1
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/AGENTS.md +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +12 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +78 -44
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +628 -31
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/compile.js +569 -0
- package/commands/computer.js +0 -60
- package/commands/improve.js +501 -0
- package/commands/integrations.js +233 -71
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/probe.js +366 -0
- package/commands/radar.js +181 -56
- package/commands/recap.js +203 -0
- package/commands/skill.js +6 -2
- package/commands/soul.js +0 -4
- package/commands/task.js +5587 -499
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +6 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +75 -1
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- package/lib/section-merge.js +0 -196
package/commands/now.js
CHANGED
|
@@ -2,7 +2,10 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const NOW_PATH = path.join('atris', 'now.md');
|
|
5
|
+
const TASK_EPISODES_PATH = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
6
|
+
const CAREER_XP_RECEIPTS_PATH = path.join('.atris', 'state', 'career_xp_receipts.jsonl');
|
|
5
7
|
const EXECUTABLE_TASK_STATUSES = new Set(['open', 'claimed']);
|
|
8
|
+
const TASK_RECEIPT_EVENTS = new Set(['proof_ready', 'reviewed', 'completed']);
|
|
6
9
|
|
|
7
10
|
function formatLocalDate(date = new Date()) {
|
|
8
11
|
const year = String(date.getFullYear());
|
|
@@ -115,6 +118,87 @@ function countJournalCompletedReceipts(filePath) {
|
|
|
115
118
|
return countMatches(filePath, /^-\s+\*\*C\d+:/gm);
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function readJsonlRows(filePath) {
|
|
122
|
+
if (!fs.existsSync(filePath)) return [];
|
|
123
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
124
|
+
.split(/\r?\n/)
|
|
125
|
+
.map(line => line.trim())
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map((line) => {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(line);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function localDateKey(value) {
|
|
138
|
+
if (!value) return null;
|
|
139
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
140
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
141
|
+
return formatLocalDate(date);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeRoot(value) {
|
|
145
|
+
if (!value) return null;
|
|
146
|
+
try {
|
|
147
|
+
return fs.realpathSync(value);
|
|
148
|
+
} catch {
|
|
149
|
+
return path.resolve(String(value));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function rowMatchesWorkspace(rowRoot, root) {
|
|
154
|
+
if (!rowRoot) return true;
|
|
155
|
+
return normalizeRoot(rowRoot) === normalizeRoot(root);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function taskReceiptProof(row) {
|
|
159
|
+
return String(
|
|
160
|
+
row?.proof
|
|
161
|
+
|| row?.proof_ref
|
|
162
|
+
|| row?.review?.proof
|
|
163
|
+
|| row?.state?.metadata?.latest_agent_proof
|
|
164
|
+
|| '',
|
|
165
|
+
).trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function taskReceiptKey(row, fallback) {
|
|
169
|
+
const episodeId = row?.episode_id || row?.source_episode_id;
|
|
170
|
+
if (episodeId) return `episode:${episodeId}`;
|
|
171
|
+
if (row?.receipt_id) return `receipt:${row.receipt_id}`;
|
|
172
|
+
if (row?.task_id || row?.source_task_id) return `task:${row.task_id || row.source_task_id}:${fallback}`;
|
|
173
|
+
return `row:${fallback}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function countTaskReceiptsToday(root = process.cwd(), date = new Date()) {
|
|
177
|
+
const targetDay = formatLocalDate(date);
|
|
178
|
+
const stateDir = path.join(root, '.atris', 'state');
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
|
|
181
|
+
for (const row of readJsonlRows(path.join(stateDir, 'task_episodes.jsonl'))) {
|
|
182
|
+
if (localDateKey(row?.created_at) !== targetDay) continue;
|
|
183
|
+
if (!rowMatchesWorkspace(row?.workspace_root, root)) continue;
|
|
184
|
+
if (!taskReceiptProof(row)) continue;
|
|
185
|
+
const eventType = String(row?.action?.event_type || '').toLowerCase();
|
|
186
|
+
if (eventType && !TASK_RECEIPT_EVENTS.has(eventType)) continue;
|
|
187
|
+
seen.add(taskReceiptKey(row, seen.size));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const row of readJsonlRows(path.join(stateDir, 'career_xp_receipts.jsonl'))) {
|
|
191
|
+
if (localDateKey(row?.accepted_at || row?.created_at || row?.ts) !== targetDay) continue;
|
|
192
|
+
if (!rowMatchesWorkspace(row?.workspace_root, root)) continue;
|
|
193
|
+
if (!taskReceiptProof(row)) continue;
|
|
194
|
+
const source = String(row?.source_type || row?.receipt_id || row?.source || '').toLowerCase();
|
|
195
|
+
if (!source.includes('task')) continue;
|
|
196
|
+
seen.add(taskReceiptKey(row, seen.size));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return seen.size;
|
|
200
|
+
}
|
|
201
|
+
|
|
118
202
|
function currentJournalPath(root = process.cwd()) {
|
|
119
203
|
const now = new Date();
|
|
120
204
|
const year = String(now.getFullYear());
|
|
@@ -129,7 +213,8 @@ function renderDefaultNow(root = process.cwd()) {
|
|
|
129
213
|
const journalPath = currentJournalPath(root);
|
|
130
214
|
const openTodoCount = countOpenWorkItems(root, todoPath);
|
|
131
215
|
const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
|
|
132
|
-
const
|
|
216
|
+
const taskReceiptCount = countTaskReceiptsToday(root);
|
|
217
|
+
const completedCount = taskReceiptCount || countJournalCompletedReceipts(journalPath);
|
|
133
218
|
const generated = todayIso();
|
|
134
219
|
|
|
135
220
|
return `# now
|
|
@@ -330,11 +415,14 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
|
|
|
330
415
|
|
|
331
416
|
module.exports = {
|
|
332
417
|
NOW_PATH,
|
|
418
|
+
TASK_EPISODES_PATH,
|
|
419
|
+
CAREER_XP_RECEIPTS_PATH,
|
|
333
420
|
ensureNowFile,
|
|
334
421
|
formatLocalDate,
|
|
335
422
|
countJournalCompletedReceipts,
|
|
336
423
|
countOpenWorkItems,
|
|
337
424
|
countOpenTodoItems,
|
|
425
|
+
countTaskReceiptsToday,
|
|
338
426
|
findChildWorkspaces,
|
|
339
427
|
isGeneratedNowFile,
|
|
340
428
|
nowAtris,
|
|
@@ -0,0 +1,366 @@
|
|
|
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
|
+
// Probe is read-only: no write/edit file ops.
|
|
61
|
+
function fileOpCommand(args, label) {
|
|
62
|
+
const op = String(args.type || '').toLowerCase();
|
|
63
|
+
const raw = normalizePath(String(args.path || '') || '.', label);
|
|
64
|
+
if (raw.split('/').includes('..')) return null;
|
|
65
|
+
const p = shq(raw);
|
|
66
|
+
if (op === 'bash') return `( ${normalizeBash(String(args.command || '') || 'true', label)} )`;
|
|
67
|
+
if (op === 'list') return `find ${p} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' | head -200`;
|
|
68
|
+
if (op === 'search') {
|
|
69
|
+
const q = shq(String(args.query || args.pattern || ''));
|
|
70
|
+
return `grep -rn -m 50 ${q} ${p} 2>/dev/null | head -50`;
|
|
71
|
+
}
|
|
72
|
+
if (op === 'read') return `{ [ -d ${p} ] && ls -p ${p} | head -200 || head -c 12000 ${p}; }`;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Port of Atris2ToolRelay.atrisCLICommand (GMModeAPI.swift) — op → `atris …`.
|
|
77
|
+
const SIMPLE_CLI_OPS = {
|
|
78
|
+
status: ['atris.md'],
|
|
79
|
+
integrations_status: ['integrations'],
|
|
80
|
+
calendar_today: ['calendar', 'today'],
|
|
81
|
+
calendar_yesterday: ['calendar', 'yesterday'],
|
|
82
|
+
calendar_week: ['calendar', 'week'],
|
|
83
|
+
gmail_inbox: ['gmail', 'inbox'],
|
|
84
|
+
slack_channels: ['slack', 'channels'],
|
|
85
|
+
slack_dms: ['slack', 'dms'],
|
|
86
|
+
task_status: ['task', 'status', '--json'],
|
|
87
|
+
task_list: ['task', 'list', '--json'],
|
|
88
|
+
skill_list: ['skill', 'list'],
|
|
89
|
+
member_list: ['member', 'list'],
|
|
90
|
+
mission_status: ['mission', 'status', '--json'],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function atrisCliCommand(args) {
|
|
94
|
+
const op = String(args.type || '').toLowerCase();
|
|
95
|
+
let argv = SIMPLE_CLI_OPS[op] || null;
|
|
96
|
+
if (!argv) {
|
|
97
|
+
if (op === 'task_show') {
|
|
98
|
+
const taskId = String(args.task_id || '');
|
|
99
|
+
if (taskId) argv = ['task', 'show', taskId, '--json'];
|
|
100
|
+
} else if (op === 'member_status') {
|
|
101
|
+
const member = String(args.member || '');
|
|
102
|
+
if (member) argv = ['member', 'status', member, '--json'];
|
|
103
|
+
} else if (op === 'calendar_date') {
|
|
104
|
+
const date = String(args.date || '');
|
|
105
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) argv = ['calendar', 'date', date];
|
|
106
|
+
} else if (op === 'slack_messages') {
|
|
107
|
+
const channel = String(args.channel || '');
|
|
108
|
+
if (channel) argv = ['slack', 'messages', channel, '--limit', '20'];
|
|
109
|
+
} else if (op === 'slack_search') {
|
|
110
|
+
const query = String(args.query || '');
|
|
111
|
+
if (query) argv = ['slack', 'search', query, '--limit', '20'];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!argv) return null;
|
|
115
|
+
return 'atris ' + argv.map(shq).join(' ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function atrisCliResult(command, term) {
|
|
119
|
+
const code = term.exit_code || 0;
|
|
120
|
+
const out = {
|
|
121
|
+
schema: 'atris.local_cli_result.v1',
|
|
122
|
+
status: code === 0 ? 'ok' : 'error',
|
|
123
|
+
command,
|
|
124
|
+
stdout: String(term.stdout || '').slice(0, 12000),
|
|
125
|
+
exit_code: code,
|
|
126
|
+
};
|
|
127
|
+
if (code !== 0) out.error = String(term.stderr || term.stdout || 'command failed').slice(0, 2000);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- HTTP helpers (Bearer auth against the prod API, like the terrace probes) ---
|
|
132
|
+
|
|
133
|
+
function postJson(urlString, token, payload, timeoutMs) {
|
|
134
|
+
const url = new URL(urlString);
|
|
135
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const body = JSON.stringify(payload);
|
|
138
|
+
const req = transport.request({
|
|
139
|
+
hostname: url.hostname,
|
|
140
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
141
|
+
path: url.pathname,
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
'Content-Type': 'application/json',
|
|
145
|
+
'Content-Length': Buffer.byteLength(body),
|
|
146
|
+
Authorization: `Bearer ${token}`,
|
|
147
|
+
},
|
|
148
|
+
timeout: timeoutMs,
|
|
149
|
+
}, (res) => {
|
|
150
|
+
let data = '';
|
|
151
|
+
res.on('data', (c) => data += c);
|
|
152
|
+
res.on('end', () => {
|
|
153
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
154
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try { resolve(data ? JSON.parse(data) : {}); } catch (e) { resolve({}); }
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
req.on('timeout', () => { req.destroy(new Error('request timeout')); });
|
|
161
|
+
req.on('error', reject);
|
|
162
|
+
req.write(body);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function runTerminal(base, token, command, businessId) {
|
|
168
|
+
const body = { command, timeout: 60 };
|
|
169
|
+
if (businessId) body.business_id = businessId;
|
|
170
|
+
return postJson(`${base}/ai-computer/terminal`, token, body, 80000);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseArgs(argv) {
|
|
174
|
+
const a = { business: null, model: 'atris:fast', memberId: null, memberSlug: null, calendar: false, message: null };
|
|
175
|
+
for (let i = 0; i < argv.length; i++) {
|
|
176
|
+
const flag = argv[i];
|
|
177
|
+
if (flag === '--business') a.business = argv[++i] || null;
|
|
178
|
+
else if (flag === '--model') a.model = argv[++i] || a.model;
|
|
179
|
+
else if (flag === '--member-id') a.memberId = argv[++i] || null;
|
|
180
|
+
else if (flag === '--member-slug') a.memberSlug = argv[++i] || null;
|
|
181
|
+
else if (flag === '--calendar') a.calendar = true;
|
|
182
|
+
else if (flag === '--message') a.message = argv[++i] || null;
|
|
183
|
+
else if (flag === '--help' || flag === '-h') {
|
|
184
|
+
console.log('Usage: atris probe [--calendar] [--business <id>] [--model atris:fast] [--member-slug <slug>] [--member-id <id>] [--message "..."]');
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return a;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function probeCommand(argv) {
|
|
192
|
+
const a = parseArgs(argv || []);
|
|
193
|
+
const creds = loadCredentials();
|
|
194
|
+
if (!creds || !creds.token) {
|
|
195
|
+
console.error('✗ Not logged in. Run: atris login');
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const token = creds.token;
|
|
199
|
+
const base = getApiBaseUrl();
|
|
200
|
+
const label = workspaceLabel(a.business);
|
|
201
|
+
const where = a.business ? `business ${String(a.business).slice(0, 8)}` : 'personal';
|
|
202
|
+
const member = a.memberSlug || a.memberId;
|
|
203
|
+
const prompt = a.message || (a.calendar
|
|
204
|
+
? "What's on my calendar today? Use your tools."
|
|
205
|
+
: 'Read the first 5 lines of any markdown file in this workspace and quote one real line from it. Use your file tools.');
|
|
206
|
+
|
|
207
|
+
const body = {
|
|
208
|
+
message: prompt, model: a.model, max_turns: 8,
|
|
209
|
+
verify_command: 'true', local_executor: true, workspace_path: label,
|
|
210
|
+
};
|
|
211
|
+
if (a.memberId) body.member_id = a.memberId;
|
|
212
|
+
if (a.memberSlug) body.member_slug = a.memberSlug;
|
|
213
|
+
|
|
214
|
+
const t0 = Date.now();
|
|
215
|
+
let toolsRun = 0;
|
|
216
|
+
let cliCalendarOps = 0;
|
|
217
|
+
let resultText = '';
|
|
218
|
+
let err = null;
|
|
219
|
+
let engine = null;
|
|
220
|
+
const unsupported = [];
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await new Promise((resolve, reject) => {
|
|
224
|
+
const url = new URL(`${base}/atris2/turn`);
|
|
225
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
226
|
+
const postData = JSON.stringify(body);
|
|
227
|
+
const req = transport.request({
|
|
228
|
+
hostname: url.hostname,
|
|
229
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
230
|
+
path: url.pathname,
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
235
|
+
Accept: 'text/event-stream',
|
|
236
|
+
Authorization: `Bearer ${token}`,
|
|
237
|
+
},
|
|
238
|
+
}, (res) => {
|
|
239
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
240
|
+
let data = '';
|
|
241
|
+
res.on('data', (c) => data += c);
|
|
242
|
+
res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
let buffer = '';
|
|
246
|
+
let idleTimer = null;
|
|
247
|
+
const IDLE_MS = 180000;
|
|
248
|
+
const resetIdle = () => {
|
|
249
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
250
|
+
idleTimer = setTimeout(() => { req.destroy(); reject(new Error(`stream stalled: no events for ${IDLE_MS / 1000}s`)); }, IDLE_MS);
|
|
251
|
+
};
|
|
252
|
+
resetIdle();
|
|
253
|
+
|
|
254
|
+
// Relayed tool calls run sequentially: the backend awaits each result
|
|
255
|
+
// before continuing the loop, so a promise chain preserves order.
|
|
256
|
+
let toolChain = Promise.resolve();
|
|
257
|
+
|
|
258
|
+
const handleEvent = (ev) => {
|
|
259
|
+
if (!ev || typeof ev !== 'object') return;
|
|
260
|
+
const et = ev.type;
|
|
261
|
+
if (et === 'system_init') {
|
|
262
|
+
engine = String(ev.model || '') || null;
|
|
263
|
+
} else if (et === 'tool_call_request') {
|
|
264
|
+
const name = String(ev.name || '');
|
|
265
|
+
const args = ev.args || ev.arguments || {};
|
|
266
|
+
const op = String(args.type || '').toLowerCase();
|
|
267
|
+
toolsRun += 1;
|
|
268
|
+
toolChain = toolChain.then(async () => {
|
|
269
|
+
let out;
|
|
270
|
+
if (name === 'local_file_op') {
|
|
271
|
+
const cmd = fileOpCommand(args, label);
|
|
272
|
+
if (cmd === null) {
|
|
273
|
+
out = { status: 'error', error: 'unsupported op or unsafe path' };
|
|
274
|
+
unsupported.push(`file_op:${op}`);
|
|
275
|
+
} else {
|
|
276
|
+
const term = await runTerminal(base, token, cmd, a.business);
|
|
277
|
+
const ok = (term.exit_code || 0) === 0;
|
|
278
|
+
out = ok
|
|
279
|
+
? { status: 'ok', stdout: String(term.stdout || '').slice(0, 12000) }
|
|
280
|
+
: { status: 'error', error: String(term.stderr || 'command failed').slice(0, 2000) };
|
|
281
|
+
}
|
|
282
|
+
} else if (name === 'local_atris_cli_op') {
|
|
283
|
+
const cmd = atrisCliCommand(args);
|
|
284
|
+
if (cmd === null) {
|
|
285
|
+
out = { status: 'error', error: `unsupported atris cli op: ${op || '?'}` };
|
|
286
|
+
unsupported.push(`cli_op:${op || '?'}`);
|
|
287
|
+
} else {
|
|
288
|
+
const term = await runTerminal(base, token, cmd, a.business);
|
|
289
|
+
out = atrisCliResult(cmd, term);
|
|
290
|
+
if (op.startsWith('calendar')) cliCalendarOps += 1;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
out = { status: 'error', error: `unsupported relayed tool: ${name || '?'}` };
|
|
294
|
+
unsupported.push(`tool:${name || '?'}`);
|
|
295
|
+
}
|
|
296
|
+
await postJson(`${base}/atris2/turn/tool-result`, token,
|
|
297
|
+
{ call_id: ev.call_id || ev.id, result: out }, 30000);
|
|
298
|
+
resetIdle();
|
|
299
|
+
}).catch((e) => reject(e));
|
|
300
|
+
} else if (et === 'result') {
|
|
301
|
+
resultText = String(ev.result || '');
|
|
302
|
+
} else if (et === 'error') {
|
|
303
|
+
err = String(ev.error || 'turn error');
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
res.setEncoding('utf8');
|
|
308
|
+
res.on('data', (chunk) => {
|
|
309
|
+
resetIdle();
|
|
310
|
+
buffer += chunk;
|
|
311
|
+
let nl;
|
|
312
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
313
|
+
const line = buffer.slice(0, nl).replace(/\r$/, '');
|
|
314
|
+
buffer = buffer.slice(nl + 1);
|
|
315
|
+
if (!line.startsWith('data: ')) continue;
|
|
316
|
+
try { handleEvent(JSON.parse(line.slice(6))); } catch (e) { /* malformed frame */ }
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
res.on('end', () => {
|
|
320
|
+
toolChain.then(() => { if (idleTimer) clearTimeout(idleTimer); resolve(); });
|
|
321
|
+
});
|
|
322
|
+
res.on('error', (e) => { if (idleTimer) clearTimeout(idleTimer); reject(e); });
|
|
323
|
+
});
|
|
324
|
+
req.on('error', reject);
|
|
325
|
+
req.write(postData);
|
|
326
|
+
req.end();
|
|
327
|
+
});
|
|
328
|
+
} catch (e) {
|
|
329
|
+
if (!err) err = `${e.name || 'Error'}: ${e.message || e}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Name the dead-end (first match wins); null = converged.
|
|
333
|
+
let deadEnd = null;
|
|
334
|
+
if (err) {
|
|
335
|
+
deadEnd = `error: ${String(err).slice(0, 120)}`;
|
|
336
|
+
} else if (a.calendar && cliCalendarOps === 0) {
|
|
337
|
+
deadEnd = 'no local_atris_cli_op calendar op relayed'
|
|
338
|
+
+ (unsupported.length ? ` (unsupported: ${unsupported.slice(0, 3).join(',')})` : '')
|
|
339
|
+
+ (toolsRun ? ` (tools=${toolsRun})` : ' (zero tool calls)');
|
|
340
|
+
} else if (!a.calendar && toolsRun === 0) {
|
|
341
|
+
deadEnd = 'zero relayed tool calls';
|
|
342
|
+
} else if (unsupported.length) {
|
|
343
|
+
deadEnd = `unsupported relayed call(s): ${unsupported.slice(0, 3).join(',')}`;
|
|
344
|
+
} else if (resultText.trim().length <= 20) {
|
|
345
|
+
deadEnd = 'empty/short final answer';
|
|
346
|
+
} else if (resultText.toLowerCase().includes(MAX_TURNS_MARKER)) {
|
|
347
|
+
deadEnd = 'max turns exhausted (honest G2 blocked message)';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const secs = Math.round((Date.now() - t0) / 100) / 10;
|
|
351
|
+
const ok = deadEnd === null;
|
|
352
|
+
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
353
|
+
const detail = ok
|
|
354
|
+
? `answer: ${resultText.slice(0, 80).replace(/\n/g, ' ')}`
|
|
355
|
+
: `dead-end: ${deadEnd}`;
|
|
356
|
+
const line = `- **atris-probe** \`${stamp}\` — ${where} · ${a.model}`
|
|
357
|
+
+ (member ? ` · member=${member}` : '')
|
|
358
|
+
+ (engine ? ` · engine=${engine}` : '')
|
|
359
|
+
+ ` · ${ok ? 'PASS' : 'FAIL'} · ${secs}s · tools=${toolsRun}`
|
|
360
|
+
+ (a.calendar ? ` cal_cli=${cliCalendarOps}` : '')
|
|
361
|
+
+ ` · ${detail}`;
|
|
362
|
+
console.log(line);
|
|
363
|
+
return ok ? 0 : 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = { probeCommand };
|