atris 3.16.0 → 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/README.md +1 -0
- package/bin/atris.js +48 -15
- package/commands/autopilot.js +431 -9
- package/commands/compile.js +569 -0
- package/commands/probe.js +366 -0
- package/commands/recap.js +203 -0
- package/commands/skill.js +6 -2
- package/commands/task.js +30 -7
- package/lib/state-detection.js +41 -1
- package/package.json +1 -1
|
@@ -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 };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const DEFAULT_DAYS = 7;
|
|
6
|
+
|
|
7
|
+
function loadTaskDb() {
|
|
8
|
+
try {
|
|
9
|
+
return require('../lib/task-db');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readProjection(root) {
|
|
16
|
+
const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
17
|
+
if (!fs.existsSync(projectionPath)) return null;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
|
|
20
|
+
return Array.isArray(parsed.tasks) ? parsed.tasks : null;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadTasks(root) {
|
|
27
|
+
const taskDb = loadTaskDb();
|
|
28
|
+
if (taskDb) {
|
|
29
|
+
try {
|
|
30
|
+
const db = taskDb.open();
|
|
31
|
+
const ws = taskDb.workspaceRoot(root);
|
|
32
|
+
const rows = taskDb.listTasks(db, { workspaceRoot: ws });
|
|
33
|
+
const refs = taskDb.taskDisplayRefMap(rows);
|
|
34
|
+
return rows.map(row => ({ ...row, display_id: refs.get(row.id) || row.id.slice(-6) }));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// fall through to projection
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return readProjection(root);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function taskProof(task) {
|
|
43
|
+
const meta = task.metadata || {};
|
|
44
|
+
const proof = String(meta.latest_agent_proof || '').trim();
|
|
45
|
+
if (proof) return proof;
|
|
46
|
+
if (meta.agent_certified === true) return 'verified by repeated agent review';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shortProof(proof, width = 70) {
|
|
51
|
+
if (!proof) return null;
|
|
52
|
+
const flat = proof.replace(/\s+/g, ' ').trim();
|
|
53
|
+
return flat.length <= width ? flat : `${flat.slice(0, width - 1)}…`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shortTitle(title, width = 64) {
|
|
57
|
+
const flat = String(title || '').replace(/\s+/g, ' ').trim();
|
|
58
|
+
return flat.length <= width ? flat : `${flat.slice(0, width - 1)}…`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildRecapData(root = process.cwd(), { days = DEFAULT_DAYS } = {}) {
|
|
62
|
+
const windowDays = Number.isFinite(Number(days)) && Number(days) > 0 ? Number(days) : DEFAULT_DAYS;
|
|
63
|
+
const tasks = loadTasks(root);
|
|
64
|
+
if (!tasks || tasks.length === 0) return { empty: true, days: windowDays, workspace: path.basename(root) };
|
|
65
|
+
|
|
66
|
+
const cutoff = Date.now() - windowDays * DAY_MS;
|
|
67
|
+
const pick = t => ({
|
|
68
|
+
id: t.display_id || t.id,
|
|
69
|
+
title: String(t.title || '').trim(),
|
|
70
|
+
proof: taskProof(t),
|
|
71
|
+
owner: t.claimed_by || (t.metadata && t.metadata.assigned_to) || null,
|
|
72
|
+
done_at: t.done_at || null,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const shipped = tasks
|
|
76
|
+
.filter(t => t.status === 'done' && Number(t.done_at || 0) >= cutoff)
|
|
77
|
+
.sort((a, b) => Number(b.done_at || 0) - Number(a.done_at || 0))
|
|
78
|
+
.map(pick);
|
|
79
|
+
const waiting = tasks
|
|
80
|
+
.filter(t => t.status === 'review')
|
|
81
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
|
|
82
|
+
.map(pick);
|
|
83
|
+
const inProgress = tasks
|
|
84
|
+
.filter(t => t.status === 'open' || t.status === 'claimed')
|
|
85
|
+
.map(pick);
|
|
86
|
+
|
|
87
|
+
const withProof = [...shipped, ...waiting].filter(t => t.proof).length;
|
|
88
|
+
return {
|
|
89
|
+
empty: false,
|
|
90
|
+
days: windowDays,
|
|
91
|
+
workspace: path.basename(root),
|
|
92
|
+
shipped,
|
|
93
|
+
waiting,
|
|
94
|
+
inProgress,
|
|
95
|
+
proof_attached: withProof,
|
|
96
|
+
proof_total: shipped.length + waiting.length,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderRecap(data) {
|
|
101
|
+
if (data.empty) {
|
|
102
|
+
return [
|
|
103
|
+
`RECAP — ${data.workspace}`,
|
|
104
|
+
'',
|
|
105
|
+
'No task history yet.',
|
|
106
|
+
'Run "atris init", then let an agent work — every finished task lands here with proof.',
|
|
107
|
+
].join('\n');
|
|
108
|
+
}
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push(`RECAP — ${data.workspace} — last ${data.days} day${data.days === 1 ? '' : 's'}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
const headline = [];
|
|
113
|
+
if (data.shipped.length) headline.push(`${data.shipped.length} change${data.shipped.length === 1 ? '' : 's'} shipped`);
|
|
114
|
+
if (data.waiting.length) headline.push(`${data.waiting.length} finished and waiting for your sign-off`);
|
|
115
|
+
if (data.inProgress.length) headline.push(`${data.inProgress.length} in progress`);
|
|
116
|
+
lines.push(headline.length ? `Your AI team: ${headline.join(' · ')}.` : 'Quiet window — no movement in this period.');
|
|
117
|
+
lines.push('Every finished line below carries proof: the commands run and their results.');
|
|
118
|
+
|
|
119
|
+
if (data.shipped.length) {
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push(`SHIPPED (accepted by a human) — ${data.shipped.length}`);
|
|
122
|
+
for (const t of data.shipped.slice(0, 12)) {
|
|
123
|
+
lines.push(` ${t.id} ${shortTitle(t.title)}`);
|
|
124
|
+
if (t.proof) lines.push(` proof: ${shortProof(t.proof)}`);
|
|
125
|
+
}
|
|
126
|
+
if (data.shipped.length > 12) lines.push(` … and ${data.shipped.length - 12} more, all with proof on file`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (data.waiting.length) {
|
|
130
|
+
lines.push('');
|
|
131
|
+
lines.push(`FINISHED, WAITING FOR YOUR SIGN-OFF — ${data.waiting.length}`);
|
|
132
|
+
for (const t of data.waiting.slice(0, 10)) {
|
|
133
|
+
lines.push(` ${t.id} ${shortTitle(t.title)}`);
|
|
134
|
+
}
|
|
135
|
+
if (data.waiting.length > 10) lines.push(` … and ${data.waiting.length - 10} more`);
|
|
136
|
+
lines.push(' approve or send back: atris task reviews');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (data.inProgress.length) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(`IN PROGRESS — ${data.inProgress.length}`);
|
|
142
|
+
for (const t of data.inProgress) {
|
|
143
|
+
lines.push(` ${t.id} ${shortTitle(t.title)}${t.owner ? ` @${t.owner}` : ''}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push('');
|
|
148
|
+
lines.push(`Proof attached: ${data.proof_attached}/${data.proof_total} finished items.`);
|
|
149
|
+
lines.push('Paste-ready summary for Slack or email: atris recap --share');
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderShare(data) {
|
|
154
|
+
if (data.empty) return `Nothing to share yet on ${data.workspace} — no finished tasks on record.`;
|
|
155
|
+
const lines = [];
|
|
156
|
+
lines.push(`What the AI team did on ${data.workspace} in the last ${data.days} day${data.days === 1 ? '' : 's'}:`);
|
|
157
|
+
lines.push('');
|
|
158
|
+
if (data.shipped.length) lines.push(`- ${data.shipped.length} change${data.shipped.length === 1 ? '' : 's'} shipped, each verified before a human accepted it`);
|
|
159
|
+
if (data.waiting.length) lines.push(`- ${data.waiting.length} more finished with proof attached, waiting for human sign-off`);
|
|
160
|
+
if (data.inProgress.length) lines.push(`- ${data.inProgress.length} task${data.inProgress.length === 1 ? '' : 's'} in progress`);
|
|
161
|
+
const highlights = [...data.shipped, ...data.waiting].filter(t => t.proof).slice(0, 5);
|
|
162
|
+
if (highlights.length) {
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('Highlights:');
|
|
165
|
+
for (const t of highlights) {
|
|
166
|
+
lines.push(`- ${shortTitle(t.title, 80)} (proof: ${shortProof(t.proof, 60)})`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
lines.push('');
|
|
170
|
+
lines.push('Every item above is backed by a receipt — the exact commands run and their results — not a status update someone typed.');
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function printRecapHelp() {
|
|
175
|
+
console.log(`
|
|
176
|
+
atris recap - what your AI team actually did, in plain English
|
|
177
|
+
|
|
178
|
+
atris recap Last 7 days: shipped, waiting on you, in progress
|
|
179
|
+
atris recap --days 30 Widen the window
|
|
180
|
+
atris recap --share Paste-ready summary for Slack, email, or a customer
|
|
181
|
+
atris recap --json Structured output for agents and dashboards
|
|
182
|
+
|
|
183
|
+
Reads the workspace task records and their proof. No jargon, no guesses:
|
|
184
|
+
if it is listed as finished, the receipt is on file.
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function recapAtris(args = []) {
|
|
189
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
190
|
+
printRecapHelp();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const daysIdx = args.indexOf('--days');
|
|
194
|
+
const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
|
|
195
|
+
const data = buildRecapData(process.cwd(), { days });
|
|
196
|
+
if (args.includes('--json')) {
|
|
197
|
+
console.log(JSON.stringify(data, null, 2));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(args.includes('--share') ? renderShare(data) : renderRecap(data));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { recapAtris, buildRecapData, renderRecap, renderShare };
|
package/commands/skill.js
CHANGED
|
@@ -183,8 +183,12 @@ function runAuditChecks(skill) {
|
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
// 7. no XML tags in content (skip placeholders like <name>, <keyword>, code blocks)
|
|
186
|
-
const
|
|
187
|
-
|
|
186
|
+
const proseContent = skill.content
|
|
187
|
+
.replace(/```[\s\S]*?```/g, '') // fenced code blocks
|
|
188
|
+
.replace(/`[^`\n]*`/g, ''); // inline code spans
|
|
189
|
+
const xmlMatches = proseContent.match(/<[a-zA-Z][^>]*>/g) || [];
|
|
190
|
+
// Single-letter tags like <X>/<N> are prose placeholders, not real XML.
|
|
191
|
+
const placeholders = /^<(name|keyword|placeholder|value|type|path|file|dir|id|url|tag|description|your-|user-|project-|skill-|[a-zA-Z]>)/i;
|
|
188
192
|
const realXml = xmlMatches.filter(t =>
|
|
189
193
|
!t.startsWith('<!--') && !t.startsWith('<!') && !placeholders.test(t)
|
|
190
194
|
);
|
package/commands/task.js
CHANGED
|
@@ -4425,15 +4425,26 @@ function cmdDelegate(args) {
|
|
|
4425
4425
|
if (handoff.swarlo) console.log(`swarlo: ${handoff.swarlo.channel}/${handoff.swarlo.action}`);
|
|
4426
4426
|
}
|
|
4427
4427
|
|
|
4428
|
-
|
|
4428
|
+
// Failed tasks older than this stop earning a daily owner-group row;
|
|
4429
|
+
// they collapse into one stale summary line instead (target state = clean day view).
|
|
4430
|
+
const DAY_STALE_FAILED_MS = 7 * 24 * 60 * 60 * 1000;
|
|
4431
|
+
|
|
4432
|
+
function taskDayGroups(tasks, { now = Date.now() } = {}) {
|
|
4429
4433
|
const active = tasks.filter(task => task.status !== 'done');
|
|
4430
|
-
const
|
|
4434
|
+
const staleFailed = [];
|
|
4435
|
+
const visible = [];
|
|
4431
4436
|
for (const task of active) {
|
|
4437
|
+
const isStaleFailed = task.status === 'failed' && (now - (task.updated_at || 0)) > DAY_STALE_FAILED_MS;
|
|
4438
|
+
if (isStaleFailed) staleFailed.push(task);
|
|
4439
|
+
else visible.push(task);
|
|
4440
|
+
}
|
|
4441
|
+
const groups = new Map();
|
|
4442
|
+
for (const task of visible) {
|
|
4432
4443
|
const owner = taskAssignee(task) || 'unassigned';
|
|
4433
4444
|
if (!groups.has(owner)) groups.set(owner, []);
|
|
4434
4445
|
groups.get(owner).push(task);
|
|
4435
4446
|
}
|
|
4436
|
-
|
|
4447
|
+
const grouped = Array.from(groups.entries())
|
|
4437
4448
|
.sort((a, b) => {
|
|
4438
4449
|
if (a[0] === 'unassigned') return 1;
|
|
4439
4450
|
if (b[0] === 'unassigned') return -1;
|
|
@@ -4446,6 +4457,7 @@ function taskDayGroups(tasks) {
|
|
|
4446
4457
|
return (statusOrder[a.status] - statusOrder[b.status]) || (b.updated_at - a.updated_at);
|
|
4447
4458
|
}),
|
|
4448
4459
|
}));
|
|
4460
|
+
return { groups: grouped, staleFailed };
|
|
4449
4461
|
}
|
|
4450
4462
|
|
|
4451
4463
|
function cmdDay(args) {
|
|
@@ -4453,13 +4465,15 @@ function cmdDay(args) {
|
|
|
4453
4465
|
const taskDb = getTaskDb();
|
|
4454
4466
|
const db = taskDb.open();
|
|
4455
4467
|
const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
|
|
4456
|
-
const groups = taskDayGroups(projection.tasks || []);
|
|
4468
|
+
const { groups, staleFailed } = taskDayGroups(projection.tasks || []);
|
|
4457
4469
|
const counts = {
|
|
4458
4470
|
active: groups.reduce((sum, group) => sum + group.tasks.length, 0),
|
|
4459
4471
|
owners: groups.length,
|
|
4460
4472
|
open: (projection.tasks || []).filter(task => task.status === 'open').length,
|
|
4461
4473
|
claimed: (projection.tasks || []).filter(task => task.status === 'claimed').length,
|
|
4462
|
-
review: (projection.tasks || []).filter(task => task.status === 'review'
|
|
4474
|
+
review: (projection.tasks || []).filter(task => task.status === 'review').length,
|
|
4475
|
+
failed: (projection.tasks || []).filter(task => task.status === 'failed').length,
|
|
4476
|
+
stale_failed: staleFailed.length,
|
|
4463
4477
|
};
|
|
4464
4478
|
const date = new Date().toISOString().slice(0, 10);
|
|
4465
4479
|
if (wantsJson(args)) {
|
|
@@ -4470,11 +4484,16 @@ function cmdDay(args) {
|
|
|
4470
4484
|
projection_path: outPath,
|
|
4471
4485
|
counts,
|
|
4472
4486
|
groups,
|
|
4487
|
+
stale_failed: {
|
|
4488
|
+
count: staleFailed.length,
|
|
4489
|
+
refs: staleFailed.map(task => taskRef(task)),
|
|
4490
|
+
},
|
|
4473
4491
|
});
|
|
4474
4492
|
return;
|
|
4475
4493
|
}
|
|
4476
4494
|
console.log('TASK DAY');
|
|
4477
|
-
|
|
4495
|
+
const failedText = counts.failed > 0 ? ` / failed ${counts.failed}` : '';
|
|
4496
|
+
console.log(`${date} active ${counts.active} / owners ${counts.owners} / review ${counts.review}${failedText}`);
|
|
4478
4497
|
console.log('');
|
|
4479
4498
|
if (!groups.length) {
|
|
4480
4499
|
console.log('clear no active tasks');
|
|
@@ -4487,6 +4506,10 @@ function cmdDay(args) {
|
|
|
4487
4506
|
console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${task.title}`);
|
|
4488
4507
|
}
|
|
4489
4508
|
}
|
|
4509
|
+
if (staleFailed.length) {
|
|
4510
|
+
console.log('');
|
|
4511
|
+
console.log(`stale ${staleFailed.length} failed >7d hidden — atris task list --status failed`);
|
|
4512
|
+
}
|
|
4490
4513
|
console.log('');
|
|
4491
4514
|
console.log('add: atris task delegate "..." --to codex --tag tasks');
|
|
4492
4515
|
}
|
|
@@ -7920,4 +7943,4 @@ async function run(args) {
|
|
|
7920
7943
|
}
|
|
7921
7944
|
}
|
|
7922
7945
|
|
|
7923
|
-
module.exports = { run };
|
|
7946
|
+
module.exports = { run, taskDayGroups };
|