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.
Files changed (51) hide show
  1. package/bin/atris.js +85 -82
  2. package/commands/aeo.js +2 -2
  3. package/commands/align.js +10 -19
  4. package/commands/auth.js +1 -1
  5. package/commands/autopilot.js +4 -8
  6. package/commands/avail.js +36 -7
  7. package/commands/computer.js +3 -1
  8. package/commands/console.js +1 -1
  9. package/commands/context-sync.js +1 -60
  10. package/commands/fleet.js +0 -1
  11. package/commands/lifecycle.js +17 -6
  12. package/commands/log-sync.js +1 -1
  13. package/commands/member.js +158 -18
  14. package/commands/mission.js +157 -69
  15. package/commands/now.js +0 -7
  16. package/commands/plugin.js +1 -1
  17. package/commands/pull.js +1 -1
  18. package/commands/push.js +0 -1
  19. package/commands/radar.js +0 -4
  20. package/commands/recap.js +25 -1
  21. package/commands/report.js +0 -4
  22. package/commands/review.js +1 -1
  23. package/commands/setup.js +6 -2
  24. package/commands/slop.js +8 -2
  25. package/commands/task.js +0 -522
  26. package/commands/terminal.js +1 -1
  27. package/commands/youtube.js +4 -6
  28. package/commands/zen.js +4 -4
  29. package/lib/agent-today.js +412 -0
  30. package/lib/autoland.js +0 -8
  31. package/lib/bench/runner.js +0 -4
  32. package/lib/chat-log-scan.js +0 -28
  33. package/lib/cli-scope.js +4 -2
  34. package/lib/engine-registry.js +0 -4
  35. package/lib/falsifier-probe.js +2 -1
  36. package/lib/file-ops.js +6 -1
  37. package/lib/first-minute.js +2 -0
  38. package/lib/gm-bet.js +125 -30
  39. package/lib/gm-phone-page.js +398 -99
  40. package/lib/gm-phone.js +30 -3
  41. package/lib/handoff-wake.js +180 -0
  42. package/lib/manifest.js +0 -10
  43. package/lib/member-switches.js +0 -11
  44. package/lib/member-up.js +18 -3
  45. package/lib/mission-protected-lane.js +38 -6
  46. package/lib/review-integrity.js +0 -4
  47. package/lib/spawn-env.js +92 -3
  48. package/lib/task-proof.js +2 -1
  49. package/lib/task-receipt.js +3 -2
  50. package/lib/yes-lock.js +633 -81
  51. 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 = {}) {
@@ -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`);
@@ -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
- return resolvedCandidate === resolvedRoot
20
- || resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`);
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 = {
@@ -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);
@@ -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
- const result = run('bash', ['-lc', cmd], {
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
- const date = dateStr ? new Date(dateStr) : new Date();
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');
@@ -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 });