atris 3.74.0 → 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 +17 -11
- package/commands/mission.js +1 -4
- 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/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/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +41 -5
- package/lib/gm-phone-page.js +14 -3
- package/lib/gm-phone.js +3 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/review-integrity.js +0 -4
- 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/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 });
|
package/lib/gm-bet.js
CHANGED
|
@@ -152,6 +152,8 @@ function readScoreboard(file, workspaceRoot) {
|
|
|
152
152
|
if (!row || row.workspace !== workspaceRoot) continue;
|
|
153
153
|
if (row.kind === 'pick' && row.name) board = { name: row.name, up: row.up !== false, source: row.source === 'installs' ? 'installs' : 'reported', at: row.at, reports: [] };
|
|
154
154
|
else if (row.kind === 'report' && board && Number.isFinite(row.value)) board.reports.push({ value: row.value, at: row.at });
|
|
155
|
+
// the owner stopped tracking it: no number until one is picked again
|
|
156
|
+
else if (row.kind === 'clear') board = null;
|
|
155
157
|
}
|
|
156
158
|
return board;
|
|
157
159
|
}
|
|
@@ -348,6 +350,8 @@ function buildQuestionRewrite(previous, reasons) {
|
|
|
348
350
|
|
|
349
351
|
const SAY_READ_MS = 15000;
|
|
350
352
|
const SAY_KINDS = new Set(['question', 'answer', 'instruction', 'unclear']);
|
|
353
|
+
// on the weekly number card, what an instruction wants done with the number
|
|
354
|
+
const NUMBER_INTENTS = new Set(['stop', 'change', 'later', 'other']);
|
|
351
355
|
const SAY_FALLBACK = 'say that another way, or tap a button.';
|
|
352
356
|
// the last turns of a card's conversation the reader sees
|
|
353
357
|
const THREAD_TURNS = 6;
|
|
@@ -380,6 +384,7 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
|
|
|
380
384
|
'- unclear: you cannot tell. reply: one short question back.',
|
|
381
385
|
'- a message that ends with "?" or asks why, what, or how is a question back, never an answer.',
|
|
382
386
|
'- you may add "suggestions": 0 to 3 short replies he could tap next, each under 40 characters.',
|
|
387
|
+
card.kind === 'number' ? '- on this number card an instruction also has "intent": "stop" when he wants to stop tracking this number, "change" when he wants a different number, "later" when he wants to be asked another time, else "other". for "other", reply says what this card can do.' : '',
|
|
383
388
|
`- plain lowercase-friendly words, no em dashes, and never these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
|
|
384
389
|
'',
|
|
385
390
|
`## the card on screen (${card.kind})`,
|
|
@@ -404,7 +409,8 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
|
|
|
404
409
|
const v = parsed.bet;
|
|
405
410
|
if (!v || !SAY_KINDS.has(v.kind) || typeof v.reply !== 'string') return { ok: false, reason: 'the read was not the shape asked for' };
|
|
406
411
|
const suggestions = Array.isArray(v.suggestions) ? v.suggestions.filter(x => typeof x === 'string' && x.trim()).map(x => x.trim()) : [];
|
|
407
|
-
|
|
412
|
+
const intent = card.kind === 'number' && NUMBER_INTENTS.has(v.intent) ? v.intent : null;
|
|
413
|
+
return { ok: true, kind: v.kind, reply: clip(v.reply, 280), ...(intent ? { intent } : {}), ...(suggestions.length ? { suggestions } : {}) };
|
|
408
414
|
}
|
|
409
415
|
|
|
410
416
|
// Two or three numbers that would tell the owner this project is going well,
|
|
@@ -2709,10 +2715,11 @@ function createBetDesk(deps) {
|
|
|
2709
2715
|
question: { key: 'scoreboard', ask: SCOREBOARD_ASK, line: null, choices: [...list.map((c, i) => ({ id: `s${i + 1}`, label: c.name })), { id: 'other', label: 'something else', other: true }], numbers: list },
|
|
2710
2716
|
};
|
|
2711
2717
|
}
|
|
2712
|
-
|
|
2718
|
+
const today = dayKey(now());
|
|
2719
|
+
// passed over today (not now, or a tap past it): asked again tomorrow
|
|
2720
|
+
if (board.source !== 'reported' || state.passedQuestions.has(`report:${today}`)) return null;
|
|
2713
2721
|
const last = board.reports[board.reports.length - 1] || null;
|
|
2714
2722
|
const weekly = !last || now() - (Date.parse(last.at) || 0) >= WEEK_MS;
|
|
2715
|
-
const today = dayKey(now());
|
|
2716
2723
|
const waiting = readLedger(deps.ledgerFile, deps.workspaceRoot).some(r => r.status === 'doing' && r.bet && r.bet.target && r.bet.target.metric === 'scoreboard'
|
|
2717
2724
|
&& String(r.bet.check_date || '') < today && !board.reports.some(x => String(x.at).slice(0, 10) >= r.bet.check_date));
|
|
2718
2725
|
if (!weekly && !waiting) return null;
|
|
@@ -2860,7 +2867,7 @@ function createBetDesk(deps) {
|
|
|
2860
2867
|
// a re-tap on a job card moves on: a yes waits for a later session, a done is seen
|
|
2861
2868
|
function passJob(item) {
|
|
2862
2869
|
if (item.kind === 'wait') return;
|
|
2863
|
-
if (item.kind === 'report') { state.passedQuestions.add(
|
|
2870
|
+
if (item.kind === 'report') { state.passedQuestions.add(item.id); return; }
|
|
2864
2871
|
if (item.kind === 'choice') {
|
|
2865
2872
|
for (const i of item.items) record(i.id, { status: 'passed', note: 'not picked' });
|
|
2866
2873
|
log({ card: 'bet', choice: 'pass', command: null, ran: 'in process', exit_code: 0, kind: 'choice' });
|
|
@@ -3279,7 +3286,7 @@ function createBetDesk(deps) {
|
|
|
3279
3286
|
if (v.kind === 'question') return { kind: 'question', title: v.question.ask, detail: v.question.line, buttons: v.question.choices.map(c => c.label) };
|
|
3280
3287
|
if (v.kind === 'bet') return { kind: 'idea', title: v.bet.title, detail: [v.bet.why_now, v.bet.expect].join(' '), buttons: ['do it', 'sharper idea', 'not now'] };
|
|
3281
3288
|
if (v.kind === 'choice') return { kind: 'choice', title: 'which one next?', detail: v.choice.options.map(o => o.title).join(' / '), buttons: [] };
|
|
3282
|
-
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: [] };
|
|
3289
|
+
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: ['save', 'not now'] };
|
|
3283
3290
|
return { kind: v.kind, title: v.job ? v.job.what : '', detail: v.job ? v.job.why : '', buttons: [] };
|
|
3284
3291
|
}
|
|
3285
3292
|
|
|
@@ -3349,6 +3356,35 @@ function createBetDesk(deps) {
|
|
|
3349
3356
|
pump();
|
|
3350
3357
|
return { ok: true, ack: plainReply(read.reply) || 'got it. the next idea will fit that.' };
|
|
3351
3358
|
}
|
|
3359
|
+
// Words on the weekly number card are about that number, and the
|
|
3360
|
+
// reader says what they want. A plain number is this week's value. Stop
|
|
3361
|
+
// tracking this, or pick a different number, stops tracking it, and the
|
|
3362
|
+
// next card asks which number, if any. Ask me tomorrow is not now. Any
|
|
3363
|
+
// other answer or instruction gets a reply on the card and changes nothing.
|
|
3364
|
+
if (kind === 'report') {
|
|
3365
|
+
const stay = (reply) => {
|
|
3366
|
+
addTurns(t, { who: 'owner', text: said }, { who: 'orb', text: reply });
|
|
3367
|
+
return { ok: true, stay: true, kind: 'unclear', reply, thread: t.turns.slice() };
|
|
3368
|
+
};
|
|
3369
|
+
if (read.kind === 'answer') {
|
|
3370
|
+
const saved = report({ value: said });
|
|
3371
|
+
if (saved.ok) { endThread(); return saved; }
|
|
3372
|
+
return stay('type this week\'s number in the box, or say stop tracking this.');
|
|
3373
|
+
}
|
|
3374
|
+
if (read.intent === 'later') {
|
|
3375
|
+
passJob(cur);
|
|
3376
|
+
state.status = 'cleared';
|
|
3377
|
+
endThread();
|
|
3378
|
+
return { ok: true, ack: 'got it. i will ask tomorrow.' };
|
|
3379
|
+
}
|
|
3380
|
+
if (read.intent !== 'stop' && read.intent !== 'change') return stay(plainReply(read.reply) || 'this card keeps one number a week. say stop tracking this, or ask me tomorrow.');
|
|
3381
|
+
const name = cur.board.name;
|
|
3382
|
+
appendLedger(scoreboardFile, { workspace: deps.workspaceRoot, kind: 'clear', name, at: iso() });
|
|
3383
|
+
log({ card: 'bet', choice: 'scoreboard_clear', command: null, ran: 'in process', exit_code: 0 });
|
|
3384
|
+
state.status = 'cleared';
|
|
3385
|
+
endThread();
|
|
3386
|
+
return { ok: true, ack: `got it. i stopped tracking ${name}.` };
|
|
3387
|
+
}
|
|
3352
3388
|
if (kind === 'yes' || kind === 'done') {
|
|
3353
3389
|
const out = await steer({ member: cur.job.owner, text: said, confirm, note });
|
|
3354
3390
|
if (out.ok) endThread();
|
package/lib/gm-phone-page.js
CHANGED
|
@@ -752,15 +752,18 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
752
752
|
b.addEventListener('click', function () {
|
|
753
753
|
if (bet.busy) return;
|
|
754
754
|
buzz();
|
|
755
|
-
|
|
755
|
+
// typed words are read first, so a question back is explained and nothing is saved
|
|
756
|
+
if (c.other) { typeHere(b, c.label, 60, function (text, field) { say(field, { text: text }); }); return; }
|
|
756
757
|
answer(b, { choice: c.id });
|
|
757
758
|
});
|
|
759
|
+
if (c.other) b.classList.add('other');
|
|
758
760
|
act.appendChild(b);
|
|
759
761
|
});
|
|
760
762
|
// a quiet line under the answers, empty until an answer does not go through
|
|
761
763
|
act.appendChild(el('div', 'links'));
|
|
762
764
|
box.appendChild(act);
|
|
763
|
-
box
|
|
765
|
+
// its own something else box is the one place to type
|
|
766
|
+
if (!q.choices.some(function (c) { return c.other; })) box.appendChild(sayBox());
|
|
764
767
|
}
|
|
765
768
|
|
|
766
769
|
// ---- answering in place: the eyes stay in one spot ----------------------------
|
|
@@ -948,7 +951,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
948
951
|
act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
|
|
949
952
|
}
|
|
950
953
|
box.appendChild(act);
|
|
951
|
-
box.appendChild(sayBox());
|
|
954
|
+
if (!own || !own.querySelector('.other')) box.appendChild(sayBox());
|
|
952
955
|
}
|
|
953
956
|
|
|
954
957
|
// the owner's own number, once a week: one box and one tap to save
|
|
@@ -978,7 +981,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
978
981
|
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
|
979
982
|
act.appendChild(input);
|
|
980
983
|
act.appendChild(save);
|
|
984
|
+
// a quiet way out: moves on, saves nothing, and is not asked again today
|
|
985
|
+
var links = el('div', 'links');
|
|
986
|
+
var later = el('button', null, 'not now');
|
|
987
|
+
later.type = 'button';
|
|
988
|
+
later.addEventListener('click', function () { if (bet.busy) return; buzz(); think(false, 0); });
|
|
989
|
+
links.appendChild(later);
|
|
990
|
+
act.appendChild(links);
|
|
981
991
|
box.appendChild(act);
|
|
992
|
+
box.appendChild(sayBox());
|
|
982
993
|
}
|
|
983
994
|
|
|
984
995
|
// A conversation in one box. While the words are read the reply spot shows
|