atris 3.79.0 → 3.80.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/lib/engine-ask.js CHANGED
@@ -868,6 +868,7 @@ async function runEngineAskCommand(args, root = process.cwd(), deps = {}) {
868
868
 
869
869
  module.exports = {
870
870
  MAX_ASK_CONCURRENCY,
871
+ askRosterPin,
871
872
  MAX_ASK_JOBS,
872
873
  MAX_ASK_PROMPT_BYTES,
873
874
  parseEngineAskArgs,
@@ -25,7 +25,7 @@ const { DEFAULT_MAX } = require('./member-up');
25
25
  // wakes, so the next handoff counts it even before its mission says running.
26
26
  const LOCK_FILE = 'handoff-wake.lock';
27
27
  const RECENT_FILE = 'handoff-wakes.json';
28
- // A holder that died, or one stuck past this long, no longer holds.
28
+ // Malformed locks get this grace period; a live owner always retains its lock.
29
29
  const STALE_LOCK_MS = 5 * 60 * 1000;
30
30
  const WAIT_LOCK_MS = 3 * 60 * 1000;
31
31
  const RECENT_MS = 30 * 60 * 1000;
@@ -51,16 +51,18 @@ function takeLock(file, { staleMs, waitMs, now }) {
51
51
  }
52
52
  try {
53
53
  const stat = fs.statSync(file);
54
+ const raw = fs.readFileSync(file, 'utf8');
54
55
  let owner = null;
55
- try { owner = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
56
+ try { owner = JSON.parse(raw); } catch (error) { if (!(error instanceof SyntaxError)) throw error; }
56
57
  const dead = owner && Number.isInteger(owner.pid) && !pidAlive(owner.pid);
57
58
  const old = now() - stat.mtimeMs > staleMs;
58
- if ((dead || old) && fs.statSync(file).ino === stat.ino) {
59
+ if ((dead || (!Number.isInteger(owner?.pid) && old)) && fs.statSync(file).ino === stat.ino) {
59
60
  fs.unlinkSync(file);
60
61
  continue;
61
62
  }
62
- } catch {
63
- continue;
63
+ } catch (error) {
64
+ if (error.code === 'ENOENT') continue;
65
+ throw error;
64
66
  }
65
67
  if (now() >= until) return null;
66
68
  sleepSync(100);
@@ -68,7 +70,7 @@ function takeLock(file, { staleMs, waitMs, now }) {
68
70
  }
69
71
 
70
72
  // Runs fn() while holding an exclusive lock file. A holder that died, or
71
- // one older than staleMs, is cleared. Throws when the lock stays held past
73
+ // malformed lock older than staleMs, is cleared. Throws when the lock stays held past
72
74
  // waitMs, so a caller never writes without it.
73
75
  function withFileLock(lockFile, fn, { staleMs = STALE_LOCK_MS, waitMs = WAIT_LOCK_MS, now = Date.now } = {}) {
74
76
  fs.mkdirSync(path.dirname(lockFile), { recursive: true });
@@ -3,7 +3,7 @@
3
3
  const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'idea', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', '99', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
4
4
  'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
5
5
  'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
6
- 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'rating', 'sync-checkout', 'zen',
6
+ 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'sentinel', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'rating', 'sync-checkout', 'zen',
7
7
  'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'google', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
8
8
  'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi', 'pay', 'trust'];
9
9
 
@@ -176,7 +176,7 @@ function latestRunReceipt(missionId, cwd) {
176
176
  // mission's own state and no commit made after it. Every tick in that run
177
177
  // must have passed its check and a verdict from a reviewer engine other than
178
178
  // the member and every engine that did the work.
179
- function reviewHold(mission, workerEngine, cwd, pin = {}) {
179
+ function reviewHold(mission, workerEngine, cwd, pin = {}, doneLooksLikes = []) {
180
180
  const receipt = latestRunReceipt(mission.id, cwd);
181
181
  if (!receipt) return 'no run receipt for this mission could be read';
182
182
  const result = receipt.result && typeof receipt.result === 'object' ? receipt.result : {};
@@ -220,6 +220,18 @@ function reviewHold(mission, workerEngine, cwd, pin = {}) {
220
220
  if (!reviewer || self.has(reviewer)) return 'the latest run was reviewed by the same engine that did the work';
221
221
  }
222
222
  if (lastTick.reviewed_git_tree !== head.stdout) return 'the review did not see the code that is checked out now';
223
+ const doneLines = [
224
+ ...(Array.isArray(doneLooksLikes) ? doneLooksLikes : []),
225
+ mission.done_looks_like,
226
+ ].map((line) => String(line || '').trim()).filter(Boolean);
227
+ if (doneLines.length) {
228
+ const { missingDoneLooksLikeClaim } = require('../commands/mission');
229
+ const reviewOutput = String(lastTick.referee_result?.output || '');
230
+ for (const doneLine of doneLines) {
231
+ const missing = missingDoneLooksLikeClaim(doneLine, { cwd, reviewOutput });
232
+ if (missing) return `missing: ${missing}`;
233
+ }
234
+ }
223
235
  pin.tree = head.stdout;
224
236
  return null;
225
237
  }
@@ -459,6 +471,7 @@ function autolandMemberRun({ mission, cwd, summary = '', workerEngine = '', load
459
471
  if (missionLane) return held('protected', `this mission sits in a protected area (${missionLane})`);
460
472
  const taskIds = linkedTaskIds(mission);
461
473
  if (!taskIds) return held('protected', 'the mission\'s linked tasks are not a clean list of ids');
474
+ const doneLooksLikes = [];
462
475
  for (const id of taskIds) {
463
476
  let task = null;
464
477
  try {
@@ -475,12 +488,18 @@ function autolandMemberRun({ mission, cwd, summary = '', workerEngine = '', load
475
488
  if (approvals.some((value) => ['revise', 'revised', 'rejected'].includes(String(value || '').trim().toLowerCase()))) {
476
489
  return held('approval', 'a linked task was turned down; it needs fresh proof before it can go to github');
477
490
  }
491
+ try {
492
+ const explanation = require('./task-explanation').taskExplanation(task);
493
+ if (explanation.sources?.done_looks_like === 'explicit' && explanation.done_looks_like) {
494
+ doneLooksLikes.push(explanation.done_looks_like);
495
+ }
496
+ } catch {}
478
497
  }
479
498
 
480
499
  const unsaved = unsavedHold(cwd);
481
500
  if (unsaved) return held('unsaved', unsaved);
482
501
  const pin = {};
483
- const reviewGap = reviewHold(mission, workerEngine, cwd, pin);
502
+ const reviewGap = reviewHold(mission, workerEngine, cwd, pin, doneLooksLikes);
484
503
  if (reviewGap) return held('review', reviewGap);
485
504
  // Pin the commit the review passed on. Only this commit (or a map-only
486
505
  // merge built on it) is pushed and merged, so a commit made after this
@@ -261,9 +261,9 @@ function progressEvent(member, raw) {
261
261
  // so a line half written before the stream started still arrives once done;
262
262
  // files that appear later are read from the top. scan() is sync, so watch
263
263
  // and poll can both call it without racing.
264
- function afterLastNewline(file) {
264
+ function afterLastNewline(file, onError = () => {}) {
265
265
  let fd;
266
- try { fd = fs.openSync(file, 'r'); } catch { return 0; }
266
+ try { fd = fs.openSync(file, 'r'); } catch (error) { onError(`open ${file}`, error); return 0; }
267
267
  try {
268
268
  const buf = Buffer.alloc(64 * 1024);
269
269
  for (let end = fs.fstatSync(fd).size; end > 0;) {
@@ -274,14 +274,16 @@ function afterLastNewline(file) {
274
274
  end = start;
275
275
  }
276
276
  return 0;
277
- } catch { return 0; } finally { fs.closeSync(fd); }
277
+ } catch (error) { onError(`read ${file}`, error); return 0; } finally { fs.closeSync(fd); }
278
278
  }
279
279
 
280
- function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = false } = {}) {
280
+ function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = false, onError = () => {} } = {}) {
281
281
  const offsets = new Map();
282
- const sizeOf = (file) => { try { return fs.statSync(file).size; } catch { return null; } };
283
- const list = () => { try { return fs.readdirSync(dir).filter((f) => f.endsWith('.log')); } catch { return []; } };
284
- for (const f of list()) offsets.set(f, afterLastNewline(path.join(dir, f)));
282
+ let directorySeen = false;
283
+ const report = (context, error) => { if (error.code !== 'ENOENT' || directorySeen) onError(context, error); };
284
+ const sizeOf = (file) => { try { return fs.statSync(file).size; } catch (error) { report(`stat ${file}`, error); return null; } };
285
+ const list = () => { try { const files = fs.readdirSync(dir); directorySeen = true; return files.filter((f) => f.endsWith('.log')); } catch (error) { report(`list ${dir}`, error); return []; } };
286
+ for (const f of list()) offsets.set(f, afterLastNewline(path.join(dir, f), report));
285
287
 
286
288
  function readNew(f, events) {
287
289
  const file = path.join(dir, f);
@@ -297,7 +299,7 @@ function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = f
297
299
  buf = Buffer.alloc(size - from);
298
300
  buf = buf.subarray(0, fs.readSync(fd, buf, 0, buf.length, from));
299
301
  } finally { fs.closeSync(fd); }
300
- } catch { return; }
302
+ } catch (error) { report(`read ${file}`, error); return; }
301
303
  // Stop at the last newline; a half-written tail waits for the next scan.
302
304
  const end = buf.lastIndexOf(0x0a);
303
305
  if (end < 0) { offsets.set(f, from); return; }
@@ -316,8 +318,8 @@ function followBoard({ dir = progressDir(), emit, pollMs = 1000, orderByTime = f
316
318
  if (!watcher) {
317
319
  try {
318
320
  watcher = fs.watch(dir, () => scan());
319
- watcher.on('error', () => { try { watcher.close(); } catch {} watcher = null; });
320
- } catch { watcher = null; }
321
+ watcher.on('error', (error) => { report(`watch ${dir}`, error); try { watcher.close(); } catch (closeError) { report(`close watcher ${dir}`, closeError); } watcher = null; });
322
+ } catch (error) { report(`watch ${dir}`, error); watcher = null; }
321
323
  }
322
324
  const events = [];
323
325
  for (const f of list()) readNew(f, events);