atris 3.73.1 → 3.74.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/bin/atris.js +85 -82
- package/commands/aeo.js +2 -2
- package/commands/align.js +10 -19
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +4 -8
- package/commands/avail.js +36 -7
- package/commands/computer.js +3 -1
- package/commands/console.js +1 -1
- package/commands/context-sync.js +1 -60
- package/commands/fleet.js +0 -1
- package/commands/lifecycle.js +17 -6
- package/commands/log-sync.js +1 -1
- package/commands/member.js +158 -18
- package/commands/mission.js +157 -69
- package/commands/now.js +0 -7
- package/commands/plugin.js +1 -1
- package/commands/pull.js +1 -1
- package/commands/push.js +0 -1
- package/commands/radar.js +0 -4
- package/commands/recap.js +25 -1
- package/commands/report.js +0 -4
- package/commands/review.js +1 -1
- package/commands/setup.js +6 -2
- package/commands/slop.js +8 -2
- package/commands/task.js +0 -522
- package/commands/terminal.js +1 -1
- package/commands/youtube.js +4 -6
- package/commands/zen.js +4 -4
- package/lib/agent-today.js +412 -0
- package/lib/autoland.js +0 -8
- package/lib/bench/runner.js +0 -4
- package/lib/chat-log-scan.js +0 -28
- package/lib/cli-scope.js +4 -2
- package/lib/engine-registry.js +0 -4
- package/lib/falsifier-probe.js +2 -1
- package/lib/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +125 -30
- package/lib/gm-phone-page.js +398 -99
- package/lib/gm-phone.js +30 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/review-integrity.js +0 -4
- package/lib/spawn-env.js +92 -3
- package/lib/task-proof.js +2 -1
- package/lib/task-receipt.js +3 -2
- package/lib/yes-lock.js +633 -81
- package/package.json +1 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// What your ai coding agents did in this folder today. Reads Claude Code
|
|
2
|
+
// and Codex session logs plus git, all local. Sends nothing anywhere and
|
|
3
|
+
// needs no atris/ folder.
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const CLAUDE_EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
10
|
+
const PATCH_FILE_RE = /\*\*\* (?:Add|Update|Delete) File: ([^\n\\"]+)/g;
|
|
11
|
+
|
|
12
|
+
function startOfToday(now = new Date()) {
|
|
13
|
+
const d = new Date(now);
|
|
14
|
+
d.setHours(0, 0, 0, 0);
|
|
15
|
+
return d;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJsonl(file) {
|
|
19
|
+
let text = '';
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(file, 'utf8');
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (const line of text.split('\n')) {
|
|
27
|
+
if (!line.trim()) continue;
|
|
28
|
+
try {
|
|
29
|
+
rows.push(JSON.parse(line));
|
|
30
|
+
} catch {
|
|
31
|
+
// half-written last line while the agent is still running
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only the first line, so a busy day of logs from other folders costs a
|
|
38
|
+
// small read each instead of the whole file.
|
|
39
|
+
function readFirstJsonLine(file, maxBytes = 1024 * 1024) {
|
|
40
|
+
let fd;
|
|
41
|
+
try {
|
|
42
|
+
fd = fs.openSync(file, 'r');
|
|
43
|
+
const chunks = [];
|
|
44
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
45
|
+
let total = 0;
|
|
46
|
+
while (total < maxBytes) {
|
|
47
|
+
const n = fs.readSync(fd, buf, 0, buf.length, total);
|
|
48
|
+
if (n <= 0) break;
|
|
49
|
+
const nl = buf.subarray(0, n).indexOf(10);
|
|
50
|
+
chunks.push(Buffer.from(buf.subarray(0, nl === -1 ? n : nl)));
|
|
51
|
+
total += n;
|
|
52
|
+
if (nl !== -1) break;
|
|
53
|
+
}
|
|
54
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
} finally {
|
|
58
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function touchedSince(file, since) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.statSync(file).mtimeMs >= since.getTime();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isToday(row, since) {
|
|
71
|
+
const t = Date.parse(row && row.timestamp);
|
|
72
|
+
return Number.isFinite(t) && t >= since.getTime();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function realPrompt(text) {
|
|
76
|
+
const s = String(text || '').trim();
|
|
77
|
+
if (!s) return false;
|
|
78
|
+
// harness wrappers and the project notes codex injects, not something
|
|
79
|
+
// the person typed
|
|
80
|
+
return !s.startsWith('<') && !s.startsWith('Caveat:') && !s.startsWith('# AGENTS.md instructions')
|
|
81
|
+
&& !s.startsWith('[Request interrupted');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Claude Code saves a typed ask as plain text, or as a list when it has a
|
|
85
|
+
// picture. A list carrying tool results is the tool talking back, not an ask.
|
|
86
|
+
function claudeAskText(content) {
|
|
87
|
+
if (typeof content === 'string') return content;
|
|
88
|
+
if (!Array.isArray(content) || content.some((c) => c && c.type === 'tool_result')) return '';
|
|
89
|
+
return content.filter((c) => c && c.type === 'text').map((c) => c.text || '').join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Claude Code marks hook feedback, image notes, and the summary of a
|
|
93
|
+
// condensed chat on the row itself. None of those are asks.
|
|
94
|
+
function claudeHarnessRow(row) {
|
|
95
|
+
return Boolean(row.isMeta || row.isCompactSummary || row.isVisibleInTranscriptOnly);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A program started this chat (claude -p, the agent sdk), not a person
|
|
99
|
+
// typing in the app.
|
|
100
|
+
function claudeStartedByProgram(row) {
|
|
101
|
+
return row.promptSource === 'sdk' || /^sdk/.test(String(row.entrypoint || ''));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Codex helpers carry a parent link and belong to another session.
|
|
105
|
+
// codex exec is a program run; the desktop app and terminal app are chats.
|
|
106
|
+
function codexOrigin(meta) {
|
|
107
|
+
const src = meta && meta.source;
|
|
108
|
+
if (src && typeof src === 'object' && src.subagent) return 'helper';
|
|
109
|
+
if ((meta && meta.originator) === 'codex_exec' || src === 'exec') return 'auto';
|
|
110
|
+
return 'you';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Claude Code keys each folder by its path with every non-alphanumeric
|
|
114
|
+
// character turned into a dash.
|
|
115
|
+
function claudeProjectDir(home, cwd, env = {}) {
|
|
116
|
+
return path.join(claudeRoot(home, env), 'projects', cwd.replace(/[^a-zA-Z0-9]/g, '-'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Past 200 characters Claude Code keeps the first 200 and adds a short
|
|
120
|
+
// code, so a deep folder is found by that prefix. Two deep folders can
|
|
121
|
+
// share it, so those logs are confirmed by the folder they record.
|
|
122
|
+
const CLAUDE_KEY_MAX = 200;
|
|
123
|
+
|
|
124
|
+
function claudeProjectDirs(home, cwd, env = {}) {
|
|
125
|
+
const exact = claudeProjectDir(home, cwd, env);
|
|
126
|
+
const key = path.basename(exact);
|
|
127
|
+
if (key.length <= CLAUDE_KEY_MAX) return { dirs: [exact], shortened: false };
|
|
128
|
+
const root = path.dirname(exact);
|
|
129
|
+
const prefix = `${key.slice(0, CLAUDE_KEY_MAX)}-`;
|
|
130
|
+
let names = [];
|
|
131
|
+
try {
|
|
132
|
+
names = fs.readdirSync(root).filter((n) => n.startsWith(prefix));
|
|
133
|
+
} catch {}
|
|
134
|
+
return { dirs: [exact, ...names.map((n) => path.join(root, n))], shortened: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sameFolder(recorded, aliases) {
|
|
138
|
+
return aliases.includes(recorded) || realOr(recorded) === aliases[0];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Both tools let people move their log folder; look where they look.
|
|
142
|
+
function claudeRoot(home, env = {}) {
|
|
143
|
+
return env.CLAUDE_CONFIG_DIR || path.join(home, '.claude');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function codexRoot(home, env = {}) {
|
|
147
|
+
return env.CODEX_HOME || path.join(home, '.codex');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readClaude({ home, env, aliases, since }) {
|
|
151
|
+
const files = new Map();
|
|
152
|
+
for (const alias of aliases) {
|
|
153
|
+
const { dirs, shortened } = claudeProjectDirs(home, alias, env);
|
|
154
|
+
for (const dir of dirs) {
|
|
155
|
+
try {
|
|
156
|
+
for (const n of fs.readdirSync(dir)) {
|
|
157
|
+
const file = path.join(dir, n);
|
|
158
|
+
if (n.endsWith('.jsonl') && !files.has(file)) files.set(file, shortened);
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const sessions = [];
|
|
164
|
+
for (const [file, shortened] of files) {
|
|
165
|
+
if (!touchedSince(file, since)) continue;
|
|
166
|
+
const rows = readJsonl(file);
|
|
167
|
+
// Folders like my-app and my_app share one log folder, so trust the
|
|
168
|
+
// folder each log records. A deep folder's log must record one.
|
|
169
|
+
const recorded = rows.find((r) => r && r.cwd);
|
|
170
|
+
if (recorded ? !sameFolder(recorded.cwd, aliases) : shortened) continue;
|
|
171
|
+
const s = { tool: 'claude', origin: null, prompts: 0, files: new Set(), lastPrompt: '' };
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
if (!isToday(row, since) || row.isSidechain) continue;
|
|
174
|
+
const content = row.message && row.message.content;
|
|
175
|
+
const ask = row.type === 'user' ? claudeAskText(content) : '';
|
|
176
|
+
if (ask && !claudeHarnessRow(row) && realPrompt(ask)) {
|
|
177
|
+
if (!s.origin) s.origin = claudeStartedByProgram(row) ? 'auto' : 'you';
|
|
178
|
+
s.prompts += 1;
|
|
179
|
+
s.lastPrompt = ask.trim();
|
|
180
|
+
}
|
|
181
|
+
if (row.type === 'assistant' && Array.isArray(content)) {
|
|
182
|
+
for (const c of content) {
|
|
183
|
+
if (c && c.type === 'tool_use' && CLAUDE_EDIT_TOOLS.has(c.name)) {
|
|
184
|
+
const p = c.input && (c.input.file_path || c.input.notebook_path);
|
|
185
|
+
if (p) s.files.add(p);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!s.origin) s.origin = 'you';
|
|
191
|
+
if (s.prompts || s.files.size) sessions.push(s);
|
|
192
|
+
}
|
|
193
|
+
return sessions;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Codex files a session under the day it started, and a session picked
|
|
197
|
+
// back up later keeps writing there. So every day folder is listed, and
|
|
198
|
+
// only logs changed since midnight are opened.
|
|
199
|
+
function codexDayDirs(home, env) {
|
|
200
|
+
const root = path.join(codexRoot(home, env), 'sessions');
|
|
201
|
+
const dirs = [];
|
|
202
|
+
const list = (p) => {
|
|
203
|
+
try {
|
|
204
|
+
return fs.readdirSync(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path.join(p, e.name));
|
|
205
|
+
} catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
for (const y of list(root)) for (const m of list(y)) dirs.push(...list(m));
|
|
210
|
+
return dirs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function insideFolder(child, folder) {
|
|
214
|
+
if (!child) return false;
|
|
215
|
+
const rel = path.relative(folder, child);
|
|
216
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readCodex({ home, env, aliases, since }) {
|
|
220
|
+
// Codex records the folder as typed, shortcut and all, so a session
|
|
221
|
+
// started through a shortcut is resolved before it is compared.
|
|
222
|
+
const here = (child) => aliases.some((a) => insideFolder(child, a))
|
|
223
|
+
|| insideFolder(realOr(child), aliases[0]);
|
|
224
|
+
const sessions = [];
|
|
225
|
+
for (const dir of codexDayDirs(home, env)) {
|
|
226
|
+
let names = [];
|
|
227
|
+
try {
|
|
228
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.jsonl'));
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const name of names) {
|
|
233
|
+
const file = path.join(dir, name);
|
|
234
|
+
if (!touchedSince(file, since)) continue;
|
|
235
|
+
const first = readFirstJsonLine(file);
|
|
236
|
+
if (first && first.type === 'session_meta' && !here(first.payload && first.payload.cwd)) continue;
|
|
237
|
+
const rows = readJsonl(file);
|
|
238
|
+
const meta = rows.find((r) => r.type === 'session_meta');
|
|
239
|
+
if (!meta || !here(meta.payload && meta.payload.cwd)) continue;
|
|
240
|
+
const s = { tool: 'codex', origin: codexOrigin(meta.payload), prompts: 0, files: new Set(), lastPrompt: '' };
|
|
241
|
+
const started = meta.payload.cwd;
|
|
242
|
+
const resolved = realOr(started) || started;
|
|
243
|
+
// edits recorded under the shortcut are moved onto the real folder
|
|
244
|
+
const onReal = (p) => (path.isAbsolute(p) && insideFolder(p, started) ? path.join(resolved, path.relative(started, p)) : p);
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
if (!isToday(row, since) || row.type !== 'response_item') continue;
|
|
247
|
+
const p = row.payload || {};
|
|
248
|
+
if (p.type === 'message' && p.role === 'user') {
|
|
249
|
+
const text = (p.content || []).map((c) => (c && c.text) || '').join('\n');
|
|
250
|
+
if (realPrompt(text)) {
|
|
251
|
+
s.prompts += 1;
|
|
252
|
+
s.lastPrompt = text.trim();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (p.type === 'custom_tool_call' || p.type === 'function_call') {
|
|
256
|
+
const raw = String(p.input || p.arguments || '');
|
|
257
|
+
for (const m of raw.matchAll(PATCH_FILE_RE)) s.files.add(onReal(m[1].trim()));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (s.prompts || s.files.size) sessions.push(s);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return sessions;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Read only: git status would otherwise refresh its own index file.
|
|
267
|
+
function git(cwd, args) {
|
|
268
|
+
const r = spawnSync('git', ['--no-optional-locks', ...args], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
269
|
+
return r.status === 0 ? r.stdout : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readGit({ cwd, since }) {
|
|
273
|
+
if (git(cwd, ['rev-parse', '--is-inside-work-tree']) === null) return null;
|
|
274
|
+
// From a subfolder of a bigger repo, count only that subfolder, so it is
|
|
275
|
+
// not credited with its neighbors' work. At the top, no limit, which
|
|
276
|
+
// would also hide merges. Every new file counts, not just its folder.
|
|
277
|
+
const sub = (git(cwd, ['rev-parse', '--show-prefix']) || '').trim();
|
|
278
|
+
const log = git(cwd, ['log', `--since=${since.toISOString()}`, '--pretty=%s', ...(sub ? ['--', '.'] : [])]) || '';
|
|
279
|
+
const commits = log.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
280
|
+
const status = git(cwd, ['status', '--porcelain', '--untracked-files=all', '--', '.']) || '';
|
|
281
|
+
const unsaved = status.split('\n').filter((l) => l.trim()).length;
|
|
282
|
+
return { commits, unsaved };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function relativeTo(cwd, file) {
|
|
286
|
+
const abs = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
287
|
+
const rel = path.relative(cwd, abs);
|
|
288
|
+
return rel.startsWith('..') ? null : rel;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function topAreas(files, limit = 2) {
|
|
292
|
+
const counts = new Map();
|
|
293
|
+
for (const f of files) {
|
|
294
|
+
const parts = f.split(path.sep);
|
|
295
|
+
const area = parts.length > 1 ? parts[0] : 'the top folder';
|
|
296
|
+
counts.set(area, (counts.get(area) || 0) + 1);
|
|
297
|
+
}
|
|
298
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([a]) => a);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function realOr(p) {
|
|
302
|
+
try {
|
|
303
|
+
return fs.realpathSync(p);
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Every name this folder goes by. The tools record the path you started
|
|
310
|
+
// in, which can run through a symlink, and the shell's PWD keeps that
|
|
311
|
+
// path even after node resolves it.
|
|
312
|
+
function folderAliases(cwd, env = {}) {
|
|
313
|
+
const real = realOr(cwd) || cwd;
|
|
314
|
+
const aliases = [real];
|
|
315
|
+
for (const p of [cwd, env.PWD]) {
|
|
316
|
+
if (p && path.isAbsolute(p) && !aliases.includes(p) && realOr(p) === real) aliases.push(p);
|
|
317
|
+
}
|
|
318
|
+
return aliases;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildAgentToday({ cwd = process.cwd(), home = os.homedir(), env = process.env, now = new Date() } = {}) {
|
|
322
|
+
const aliases = folderAliases(cwd, env);
|
|
323
|
+
const folder = aliases[0];
|
|
324
|
+
const since = startOfToday(now);
|
|
325
|
+
const sessions = [
|
|
326
|
+
...readClaude({ home, env, aliases, since }),
|
|
327
|
+
...readCodex({ home, env, aliases, since }),
|
|
328
|
+
];
|
|
329
|
+
const files = new Set();
|
|
330
|
+
for (const s of sessions) {
|
|
331
|
+
for (const f of s.files) {
|
|
332
|
+
const rel = aliases.map((a) => relativeTo(a, f)).find(Boolean);
|
|
333
|
+
if (rel) files.add(rel);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const gitInfo = readGit({ cwd: folder, since });
|
|
337
|
+
const yours = sessions.filter((s) => s.origin === 'you');
|
|
338
|
+
return {
|
|
339
|
+
folder: path.basename(folder),
|
|
340
|
+
since: since.toISOString(),
|
|
341
|
+
sessions: {
|
|
342
|
+
claude: yours.filter((s) => s.tool === 'claude').length,
|
|
343
|
+
codex: yours.filter((s) => s.tool === 'codex').length,
|
|
344
|
+
},
|
|
345
|
+
prompts: yours.reduce((n, s) => n + s.prompts, 0),
|
|
346
|
+
autoRuns: sessions.filter((s) => s.origin === 'auto').length,
|
|
347
|
+
files: [...files].sort(),
|
|
348
|
+
areas: topAreas([...files]),
|
|
349
|
+
commits: gitInfo ? gitInfo.commits : [],
|
|
350
|
+
unsaved: gitInfo ? gitInfo.unsaved : 0,
|
|
351
|
+
isGit: Boolean(gitInfo),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function hasActivity(t) {
|
|
356
|
+
return Boolean(t && (ranAgent(t) || t.commits.length > 0));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ranAgent(t) {
|
|
360
|
+
return t.sessions.claude + t.sessions.codex + (t.autoRuns || 0) > 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function plural(n, one, many = `${one}s`) {
|
|
364
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function quote(text, width = 60) {
|
|
368
|
+
const s = String(text).replace(/\s+/g, ' ').trim();
|
|
369
|
+
return s.length > width ? `${s.slice(0, width - 3).trimEnd()}...` : s;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Five lines at most, plain words, one idea each.
|
|
373
|
+
function renderAgentToday(t) {
|
|
374
|
+
const lines = [`today in ${t.folder}`];
|
|
375
|
+
const total = t.sessions.claude + t.sessions.codex;
|
|
376
|
+
if (!hasActivity(t)) {
|
|
377
|
+
lines.push('no ai sessions or saved changes here yet today.');
|
|
378
|
+
lines.push('run this again after your agent does some work.');
|
|
379
|
+
return lines.join('\n');
|
|
380
|
+
}
|
|
381
|
+
if (total) {
|
|
382
|
+
const who = [
|
|
383
|
+
t.sessions.claude ? `${plural(t.sessions.claude, 'session')} in claude code` : '',
|
|
384
|
+
t.sessions.codex ? `${plural(t.sessions.codex, 'session')} in codex` : '',
|
|
385
|
+
].filter(Boolean).join(' and ');
|
|
386
|
+
lines.push(`you asked your ai agent for ${plural(t.prompts, 'thing')}, across ${who}.`);
|
|
387
|
+
}
|
|
388
|
+
if (t.autoRuns) {
|
|
389
|
+
lines.push(`your agents ${total ? 'also ' : ''}ran ${plural(t.autoRuns, 'job')} on their own.`);
|
|
390
|
+
}
|
|
391
|
+
if (t.files.length) {
|
|
392
|
+
const where = t.files.length === 1 ? ` in ${t.areas[0]}` : `, mostly in ${t.areas.join(' and ')}`;
|
|
393
|
+
lines.push(`it changed ${plural(t.files.length, 'file')}${where}.`);
|
|
394
|
+
} else if (ranAgent(t)) {
|
|
395
|
+
lines.push('it read and ran things but changed no files.');
|
|
396
|
+
}
|
|
397
|
+
const saved = t.commits.length
|
|
398
|
+
? `${plural(t.commits.length, 'change')} saved to git. latest: "${quote(t.commits[0])}"`
|
|
399
|
+
: '';
|
|
400
|
+
const unsaved = t.unsaved ? `${plural(t.unsaved, 'file')} changed but not saved to git yet.` : '';
|
|
401
|
+
if (saved && unsaved && lines.length + 2 > 5) {
|
|
402
|
+
// keep five lines: one git line says both
|
|
403
|
+
lines.push(`${plural(t.commits.length, 'change')} saved to git, ${plural(t.unsaved, 'file')} not saved yet.`);
|
|
404
|
+
} else {
|
|
405
|
+
if (saved) lines.push(saved);
|
|
406
|
+
if (unsaved) lines.push(unsaved);
|
|
407
|
+
else if (t.isGit && t.files.length) lines.push('everything it changed is saved to git.');
|
|
408
|
+
}
|
|
409
|
+
return lines.slice(0, 5).join('\n');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = { buildAgentToday, renderAgentToday, hasActivity, ranAgent, claudeProjectDir, startOfToday, readFirstJsonLine, folderAliases };
|
package/lib/autoland.js
CHANGED
|
@@ -492,14 +492,6 @@ function unexplainedSummaryRows(items, label) {
|
|
|
492
492
|
return [`${plur(count, label)} stayed hidden because ${count === 1 ? 'its meaning was' : 'their meanings were'} unclear`];
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
function missionDigestLine(mission, max = 160) {
|
|
496
|
-
const visibleGoal = mission?.visible_goal?.desired_objective
|
|
497
|
-
|| mission?.goal?.visible_goal?.desired_objective
|
|
498
|
-
|| (typeof mission?.visible_goal === 'string' ? mission.visible_goal : '');
|
|
499
|
-
const objective = visibleGoal || mission?.objective || '';
|
|
500
|
-
return finishThought(clarify(objective, max), max);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
495
|
// Some older surfaces still need a readable fallback for rough text, but the
|
|
504
496
|
// digest paths above now hide agent-speak until a result sentence exists.
|
|
505
497
|
function dejargon(line, options = {}) {
|
package/lib/bench/runner.js
CHANGED
|
@@ -30,10 +30,6 @@ function packDir(repoRoot = repoRootFromHere(), pack = DEFAULT_PACK) {
|
|
|
30
30
|
return path.join(benchmarksRoot(repoRoot), pack || DEFAULT_PACK);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function defaultTasksDir(repoRoot = repoRootFromHere()) {
|
|
34
|
-
return packDir(repoRoot, DEFAULT_PACK);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
function validateTaskSpec(spec, filePath) {
|
|
38
34
|
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
39
35
|
throw new BenchInfraError(`${filePath}: task spec must export an object`);
|
package/lib/chat-log-scan.js
CHANGED
|
@@ -341,34 +341,6 @@ function writeLatestScan(root, report) {
|
|
|
341
341
|
return latestPath;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
function readLatestScan(root) {
|
|
345
|
-
const latestPath = path.join(root, '.atris', 'state', 'chat_scan.latest.json');
|
|
346
|
-
try {
|
|
347
|
-
if (!fs.existsSync(latestPath)) return null;
|
|
348
|
-
return JSON.parse(fs.readFileSync(latestPath, 'utf8'));
|
|
349
|
-
} catch {
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function ensureChatScan(root, options = {}) {
|
|
355
|
-
const maxAgeMs = Number(options.maxAgeMs ?? 3600000);
|
|
356
|
-
const existing = readLatestScan(root);
|
|
357
|
-
if (existing && existing.scanned_at && !options.force) {
|
|
358
|
-
const age = Date.now() - new Date(existing.scanned_at).getTime();
|
|
359
|
-
if (Number.isFinite(age) && age >= 0 && age <= maxAgeMs) {
|
|
360
|
-
return existing;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const report = scanChatLogs({ cwd: root, hours: options.hours, limit: options.limit });
|
|
364
|
-
if (options.write !== false) writeLatestScan(root, report);
|
|
365
|
-
return report;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function topChatFindings(report, limit = 3) {
|
|
369
|
-
return (report && Array.isArray(report.findings) ? report.findings : []).slice(0, limit);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
344
|
module.exports = {
|
|
373
345
|
SCHEMA,
|
|
374
346
|
scanChatLogs,
|
package/lib/cli-scope.js
CHANGED
|
@@ -16,8 +16,10 @@ function pathUnderRoot(candidate, root) {
|
|
|
16
16
|
if (!candidate || !root) return false;
|
|
17
17
|
const resolvedCandidate = path.resolve(String(candidate));
|
|
18
18
|
const resolvedRoot = path.resolve(String(root));
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
if (resolvedCandidate === resolvedRoot) return true;
|
|
20
|
+
// A filesystem root already ends in a separator ('/' or 'C:\\').
|
|
21
|
+
const prefix = resolvedRoot.endsWith(path.sep) ? resolvedRoot : `${resolvedRoot}${path.sep}`;
|
|
22
|
+
return resolvedCandidate.startsWith(prefix);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
module.exports = {
|
package/lib/engine-registry.js
CHANGED
|
@@ -1748,10 +1748,6 @@ function engineRegistryView(root = process.cwd()) {
|
|
|
1748
1748
|
return readEngineRegistry(root).engines;
|
|
1749
1749
|
}
|
|
1750
1750
|
|
|
1751
|
-
function registeredEngineIds(root = process.cwd()) {
|
|
1752
|
-
return engineRegistryView(root).map((engine) => engine.id);
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
1751
|
function resolveRegisteredEngine(name, root = process.cwd()) {
|
|
1756
1752
|
const requested = String(name || '').trim();
|
|
1757
1753
|
const id = canonicalEngineName(requested);
|
package/lib/falsifier-probe.js
CHANGED
|
@@ -52,7 +52,8 @@ function probeVerifierCanFail({ command, runner, tmpRoot } = {}) {
|
|
|
52
52
|
// like it cannot fail. Same strip as verifyCommandEnv.
|
|
53
53
|
const env = { ...process.env };
|
|
54
54
|
delete env.NODE_TEST_CONTEXT;
|
|
55
|
-
|
|
55
|
+
// a login shell rebuilds PATH; a mission guard's wrappers go back first
|
|
56
|
+
const result = run('bash', ['-lc', `${require('./spawn-env').shellGuardLine(env)}${cmd}`], {
|
|
56
57
|
cwd: dir,
|
|
57
58
|
encoding: 'utf8',
|
|
58
59
|
timeout: PROBE_TIMEOUT_MS,
|
package/lib/file-ops.js
CHANGED
|
@@ -9,7 +9,12 @@ const escapeRegExp = require('./escape-regexp');
|
|
|
9
9
|
*/
|
|
10
10
|
function getLogPath(dateStr) {
|
|
11
11
|
const targetDir = path.join(process.cwd(), 'atris');
|
|
12
|
-
|
|
12
|
+
// A bare YYYY-MM-DD is that calendar day here. new Date('2026-10-22') is
|
|
13
|
+
// UTC midnight, which is the evening before anywhere west of Greenwich.
|
|
14
|
+
const dayOnly = typeof dateStr === 'string' && dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
15
|
+
const date = dayOnly
|
|
16
|
+
? new Date(Number(dayOnly[1]), Number(dayOnly[2]) - 1, Number(dayOnly[3]))
|
|
17
|
+
: (dateStr ? new Date(dateStr) : new Date());
|
|
13
18
|
const year = date.getFullYear();
|
|
14
19
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
15
20
|
const day = String(date.getDate()).padStart(2, '0');
|
package/lib/first-minute.js
CHANGED
|
@@ -178,6 +178,8 @@ function loadLocalTasks(root = process.cwd()) {
|
|
|
178
178
|
let dbRows = [];
|
|
179
179
|
try {
|
|
180
180
|
const taskDb = require('./task-db');
|
|
181
|
+
// Reading never creates the db, so a first look writes nothing.
|
|
182
|
+
if (!fs.existsSync(taskDb.getDbPath())) throw new Error('no task db yet');
|
|
181
183
|
const db = taskDb.open();
|
|
182
184
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
183
185
|
const rows = taskDb.listTasks(db, { workspaceRoot, limit: 200 });
|