atris 3.48.0 → 3.49.0

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.
@@ -1,6 +1,7 @@
1
1
  const { apiRequestJson } = require('../utils/api');
2
2
  const { ensureValidCredentials } = require('../utils/auth');
3
3
  const { spawnSync } = require('child_process');
4
+ const fs = require('fs');
4
5
  const path = require('path');
5
6
  const https = require('https');
6
7
 
@@ -444,6 +445,67 @@ function formatYoutubeResult(data) {
444
445
  return lines.join('\n');
445
446
  }
446
447
 
448
+ function videoIdFromUrl(url) {
449
+ const text = String(url || '');
450
+ const watch = text.match(/[?&]v=([^&]+)/);
451
+ if (watch) return watch[1];
452
+ const short = text.match(/youtu\.be\/([^?&/]+)/);
453
+ return short ? short[1] : null;
454
+ }
455
+
456
+ function dateStamp(now) {
457
+ if (typeof now === 'string' && /^\d{4}-\d{2}-\d{2}/.test(now)) {
458
+ return now.slice(0, 10);
459
+ }
460
+ const value = now instanceof Date ? now : new Date(now || Date.now());
461
+ if (Number.isNaN(value.getTime())) return new Date().toISOString().slice(0, 10);
462
+ return value.toISOString().slice(0, 10);
463
+ }
464
+
465
+ function firstHeading(notes) {
466
+ const match = String(notes || '').replace(/\r\n/g, '\n').match(/^#{1,6}\s+(.+)$/m);
467
+ return match ? match[1].trim() : '';
468
+ }
469
+
470
+ function fileBriefFromNotes({ cwd, url, workDir, now } = {}) {
471
+ try {
472
+ const id = videoIdFromUrl(url);
473
+ if (!id) return;
474
+ const notesPath = path.join(workDir, `yt_${id}.md`);
475
+ if (!fs.existsSync(notesPath)) return;
476
+ const notes = fs.readFileSync(notesPath, 'utf8');
477
+ const wikiDir = path.join(cwd, 'atris', 'wiki');
478
+ if (!fs.existsSync(wikiDir)) return;
479
+
480
+ const heading = firstHeading(notes);
481
+ const date = dateStamp(now);
482
+ const header = [
483
+ heading.toLowerCase(),
484
+ '',
485
+ `date: ${date}`,
486
+ `source: ${url}`,
487
+ 'rail: atris youtube notes, quotes repaired against the transcript',
488
+ ].join('\n');
489
+ const briefsDir = path.join(wikiDir, 'briefs');
490
+ fs.mkdirSync(briefsDir, { recursive: true });
491
+ const relBrief = `atris/wiki/briefs/youtube-${id}.md`;
492
+ fs.writeFileSync(path.join(cwd, relBrief), `${header}\n${notes}`);
493
+
494
+ const year = date.slice(0, 4);
495
+ const journalPath = path.join(cwd, 'atris', 'logs', year, `${date}.md`);
496
+ fs.mkdirSync(path.dirname(journalPath), { recursive: true });
497
+ let existing = '';
498
+ if (fs.existsSync(journalPath)) existing = fs.readFileSync(journalPath, 'utf8');
499
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
500
+ const line = `- [claimable] watched: ${heading} -> ${relBrief}`;
501
+ fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
502
+
503
+ console.log(`brief filed: ${relBrief}`);
504
+ } catch {
505
+ // notes filing must never break the youtube command
506
+ }
507
+ }
508
+
447
509
  function runYoutubeNotes(args = [], deps = {}) {
448
510
  const output = deps.output || ((line = '') => console.error(line));
449
511
  const url = args[0];
@@ -459,6 +521,14 @@ function runYoutubeNotes(args = [], deps = {}) {
459
521
  const childArgs = engine ? [url, engine] : [url];
460
522
  const result = spawn(script, childArgs, { stdio: 'inherit' });
461
523
  if (result.status == null) return 1;
524
+ if (result.status === 0) {
525
+ fileBriefFromNotes({
526
+ cwd: deps.cwd || process.cwd(),
527
+ url,
528
+ workDir: deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes'),
529
+ now: deps.now || new Date(),
530
+ });
531
+ }
462
532
  return result.status;
463
533
  }
464
534
 
@@ -488,5 +558,6 @@ module.exports = {
488
558
  processYoutube,
489
559
  shouldRetryWithLocalTranscript,
490
560
  formatYoutubeResult,
561
+ fileBriefFromNotes,
491
562
  youtubeCommand,
492
563
  };
@@ -33,6 +33,7 @@ function engineFailureHealthStatus(result) {
33
33
  if (/usage[ _-]?limit|purchase more credits|insufficient credits|credit(?:s)?[ _-]?(?:out|limit)|rate[ _-]?limit|not authenticated|please log in|login required|auth(?:entication)?[ _-]?expired|payment required|subscription/.test(signalText)) {
34
34
  return 'credit_out';
35
35
  }
36
+ if (/not installed|command not found|\benoent\b/.test(signalText)) return 'not_installed';
36
37
  if (/timeout|model-unavailable/.test(signalText)) return 'not_installed';
37
38
  return 'error';
38
39
  }
package/lib/fleet.js CHANGED
@@ -208,7 +208,7 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
208
208
  if (sealed && engineName === 'codex') {
209
209
  cmd = cmd.replace(/\bexec\b/, 'exec --sandbox workspace-write --ephemeral --ignore-user-config --ignore-rules');
210
210
  }
211
- if (sealed && engineName === 'claude') {
211
+ if (sealed && (engineName === 'claude' || engineName === 'fable')) {
212
212
  cmd = `${cmd} --safe-mode --no-session-persistence --permission-mode acceptEdits --settings '${JSON.stringify({ sandbox: { enabled: true, autoAllowBashIfSandboxed: true } })}'`;
213
213
  }
214
214
  if (sealed && engineName === 'cursor') cmd = `${cmd} --sandbox enabled`;
@@ -218,7 +218,7 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
218
218
  }
219
219
  if (engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --permission-mode dangerous ');
220
220
  if (yolo && engineName === 'codex') cmd = cmd.replace(/\bexec\b/, `exec ${YOLO_ENGINE_FLAGS.codex}`);
221
- if (yolo && engineName === 'claude') cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
221
+ if (yolo && (engineName === 'claude' || engineName === 'fable')) cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
222
222
  if (engineName === 'codex') cmd = wrapCodexWithWatchdog(cmd, watchdogPath, watchdogReceiptPath);
223
223
  return cmd;
224
224
  } finally {
@@ -561,6 +561,25 @@ function detectDeadEngineDispatch(result) {
561
561
  return { reason: 'nonzero_exit', exitCode };
562
562
  }
563
563
 
564
+ function plainDispatchFailureCause(result, failure = {}) {
565
+ const reason = String(failure.reason || failure.stage || '').trim();
566
+ const output = dispatchResultOutput(result);
567
+ if (reason === 'no_output') return 'the engine returned no output';
568
+ if (/not authenticated|please log in|login required|auth(?:entication)?[ _-]?expired/i.test(output)) {
569
+ return 'the engine login expired';
570
+ }
571
+ if (/spawn[^\n]*enoent|enoent[^\n]*spawn|failed to spawn/i.test(output)) {
572
+ return 'the engine could not start';
573
+ }
574
+ if (reason === 'timeout') return 'the engine timed out';
575
+ if (reason === 'cancelled') return 'the engine run was cancelled';
576
+ if (reason === 'unknown') return 'the engine exited without a status';
577
+ if (reason === 'signalled') return `the engine stopped with ${failure.signal || 'a signal'}`;
578
+ const detail = String(failure.detail || output || '').trim().split('\n')[0].trim();
579
+ if (detail) return detail.replace(/[.]+$/, '');
580
+ return (reason || 'the engine failed').replace(/_/g, ' ');
581
+ }
582
+
564
583
  function recordDispatchEngineHealth(result, failure, root) {
565
584
  if (!result || !result.engine) return null;
566
585
  const status = failure
@@ -1081,6 +1100,7 @@ module.exports = {
1081
1100
  shipWithRetry,
1082
1101
  shipFailureDetail,
1083
1102
  get FLEET_CAPABLE() { return FLEET_CAPABLE; },
1103
+ get DISPATCH_CAPABLE() { return DISPATCH_CAPABLE; },
1084
1104
  get runFleetFlight() { return runFleetFlight; },
1085
1105
  get focusedCheck() { return focusedCheck; },
1086
1106
  get dispatchCheck() { return dispatchCheck; },
@@ -1122,6 +1142,7 @@ module.exports = {
1122
1142
  // Engines that can edit a repo headlessly. atris-fast (ax) is a chat lane,
1123
1143
  // not a repo worker — it keeps owning normal mission ticks, not fleet builds.
1124
1144
  const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin', 'grok'];
1145
+ const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable'];
1125
1146
 
1126
1147
  let receiptSequence = 0;
1127
1148
  function nowStamp() {
@@ -2658,6 +2679,18 @@ async function runFleetFlight({
2658
2679
  // ---------------------------------------------------------------------------
2659
2680
  // T5 — one-command dispatch: `atris engine dispatch <task-id> --engine <name>`
2660
2681
 
2682
+ // A claim taken before the engine starts must not stay held if the flight
2683
+ // refuses or errors first. Release through the same task plane the claim used.
2684
+ function releaseUnstartedDispatchClaim(cli, { taskId, actor, detail }) {
2685
+ const released = cli(['task', 'release', taskId, '--as', actor]);
2686
+ const releasedOk = Boolean(released && released.status === 0);
2687
+ const suffix = releasedOk
2688
+ ? 'claim released, safe to retry'
2689
+ : `claim release failed: ${String(released && (released.stderr || released.stdout) || 'unknown').trim().slice(0, 80)}`;
2690
+ const base = String(detail || '').trim().slice(0, 200);
2691
+ return { released: releasedOk, detail: base ? `${base}. ${suffix}` : suffix };
2692
+ }
2693
+
2661
2694
  // The manual version of this loop took 6 Bash calls per task the night this
2662
2695
  // was written: claim, worktree start, prompt file, engine -p, verify, ship.
2663
2696
  // One or more explicit task ids build in parallel isolated worktrees on ONE
@@ -2693,8 +2726,8 @@ async function runDispatchFlight({
2693
2726
  scoutAsk = null,
2694
2727
  } = {}) {
2695
2728
  if (!engine) throw new Error('runDispatchFlight: engine is required');
2696
- if (!FLEET_CAPABLE.includes(engine)) {
2697
- throw new Error(`runDispatchFlight: engine "${engine}" cannot build headlessly (capable: ${FLEET_CAPABLE.join(', ')})`);
2729
+ if (!DISPATCH_CAPABLE.includes(engine)) {
2730
+ throw new Error(`runDispatchFlight: engine "${engine}" cannot build headlessly (capable: ${DISPATCH_CAPABLE.join(', ')})`);
2698
2731
  }
2699
2732
  const ids = [...new Set((taskIds || []).map((id) => String(id).trim()).filter(Boolean))];
2700
2733
  if (!ids.length) throw new Error('runDispatchFlight: at least one task id is required');
@@ -2760,51 +2793,71 @@ async function runDispatchFlight({
2760
2793
  log(` x ${taskId} claim failed`);
2761
2794
  continue;
2762
2795
  }
2763
- let landingWorktreePath = '';
2764
- let remoteBoundary = null;
2765
- if (enforceRemoteBoundary) {
2766
- remoteBoundary = prepareReviewSandbox({ root, taskId, engine });
2767
- } else {
2768
- const started = cli(['worktree', 'start', '--agent', engine, '--task', `dispatch-${taskId.toLowerCase()}`, ...startBaseArgs]);
2769
- const wt = (started.stdout.match(/next: cd (.+)/) || [])[1];
2770
- if (!wt) {
2771
- flight.paused.push({ task: taskId, stage: 'worktree_start', detail: String(started.stderr || '').slice(0, 200) });
2772
- log(` ✗ ${taskId} worktree start failed`);
2796
+ try {
2797
+ let landingWorktreePath = '';
2798
+ let remoteBoundary = null;
2799
+ if (enforceRemoteBoundary) {
2800
+ remoteBoundary = prepareReviewSandbox({ root, taskId, engine });
2801
+ } else {
2802
+ const started = cli(['worktree', 'start', '--agent', engine, '--task', `dispatch-${taskId.toLowerCase()}`, ...startBaseArgs]);
2803
+ const wt = (started.stdout.match(/next: cd (.+)/) || [])[1];
2804
+ if (!wt) {
2805
+ const released = releaseUnstartedDispatchClaim(cli, {
2806
+ taskId,
2807
+ actor: taskActor,
2808
+ detail: String(started.stderr || '').slice(0, 200),
2809
+ });
2810
+ flight.paused.push({ task: taskId, stage: 'worktree_start', detail: released.detail });
2811
+ log(` ✗ ${taskId} worktree start failed`);
2812
+ continue;
2813
+ }
2814
+ landingWorktreePath = wt.trim();
2815
+ }
2816
+ if (enforceRemoteBoundary && (!remoteBoundary || remoteBoundary.ok !== true)) {
2817
+ const released = releaseUnstartedDispatchClaim(cli, {
2818
+ taskId,
2819
+ actor: taskActor,
2820
+ detail: String(remoteBoundary && remoteBoundary.detail || 'could not prepare a sealed review sandbox').slice(-500),
2821
+ });
2822
+ flight.paused.push({
2823
+ task: taskId,
2824
+ stage: 'remote_quarantine',
2825
+ detail: released.detail,
2826
+ worktree: null,
2827
+ });
2828
+ log(` paused ${taskId} because its sealed review sandbox could not be prepared`);
2773
2829
  continue;
2774
2830
  }
2775
- landingWorktreePath = wt.trim();
2776
- }
2777
- if (enforceRemoteBoundary && (!remoteBoundary || remoteBoundary.ok !== true)) {
2778
- flight.paused.push({
2779
- task: taskId,
2780
- stage: 'remote_quarantine',
2781
- detail: String(remoteBoundary && remoteBoundary.detail || 'could not prepare a sealed review sandbox').slice(-500),
2782
- worktree: null,
2831
+ const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
2832
+ const startCommit = yolo ? readStartCommit({ worktreePath }) : '';
2833
+ const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
2834
+ const safetyPrompt = reviewOnly
2835
+ ? `${basePrompt}\n\nSafety boundary: edit and test this checkout only. Do not push, merge, deploy, publish, send messages, change cloud state, install dependencies, or use credentials.`
2836
+ : basePrompt;
2837
+ const trustedPrompt = trustedVerifier
2838
+ ? `${safetyPrompt}\n\nTrusted verifier (run it bare before reporting done): ${trustedVerifier}`
2839
+ : promptOverride;
2840
+ prepared.push({
2841
+ task,
2842
+ taskId,
2843
+ worktreePath,
2844
+ landingWorktreePath,
2845
+ engine,
2846
+ remoteBoundary,
2847
+ remoteMasterBefore: remoteBoundary ? remoteBoundary.protectedMaster : '',
2848
+ startCommit,
2849
+ ...(trustedPrompt ? { prompt: trustedPrompt } : {}),
2783
2850
  });
2784
- log(` paused ${taskId} because its sealed review sandbox could not be prepared`);
2785
- continue;
2851
+ log(` building ${taskId} in ${path.basename(worktreePath)}`);
2852
+ } catch (err) {
2853
+ const released = releaseUnstartedDispatchClaim(cli, {
2854
+ taskId,
2855
+ actor: taskActor,
2856
+ detail: String(err && err.message || err).slice(0, 200),
2857
+ });
2858
+ flight.paused.push({ task: taskId, stage: 'prepare', detail: released.detail });
2859
+ log(` ✗ ${taskId} prepare failed`);
2786
2860
  }
2787
- const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
2788
- const startCommit = yolo ? readStartCommit({ worktreePath }) : '';
2789
- const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
2790
- const safetyPrompt = reviewOnly
2791
- ? `${basePrompt}\n\nSafety boundary: edit and test this checkout only. Do not push, merge, deploy, publish, send messages, change cloud state, install dependencies, or use credentials.`
2792
- : basePrompt;
2793
- const trustedPrompt = trustedVerifier
2794
- ? `${safetyPrompt}\n\nTrusted verifier (run it bare before reporting done): ${trustedVerifier}`
2795
- : promptOverride;
2796
- prepared.push({
2797
- task,
2798
- taskId,
2799
- worktreePath,
2800
- landingWorktreePath,
2801
- engine,
2802
- remoteBoundary,
2803
- remoteMasterBefore: remoteBoundary ? remoteBoundary.protectedMaster : '',
2804
- startCommit,
2805
- ...(trustedPrompt ? { prompt: trustedPrompt } : {}),
2806
- });
2807
- log(` building ${taskId} in ${path.basename(worktreePath)}`);
2808
2861
  }
2809
2862
 
2810
2863
  const dispatch = dispatcher || ((entry) => new Promise((resolve) => {
@@ -2828,6 +2881,10 @@ async function runDispatchFlight({
2828
2881
  }));
2829
2882
  const restaffState = { used: false };
2830
2883
 
2884
+ if (engine === 'fable' && prepared.length) {
2885
+ log(` fable handoff started: receipt ${path.relative(root, receiptPath)}`);
2886
+ }
2887
+
2831
2888
  const results = await Promise.all(prepared.map((entry) => {
2832
2889
  const startedAtMs = Date.now();
2833
2890
  return dispatchEntryWithRestaff({
@@ -3343,6 +3400,12 @@ async function runDispatchFlight({
3343
3400
  flight.finished_at = new Date().toISOString();
3344
3401
  writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
3345
3402
  log('');
3403
+ if (engine === 'fable' && flight.paused.length) {
3404
+ const paused = flight.paused[0];
3405
+ const failed = results.find(({ entry }) => entry.taskId === paused.task);
3406
+ const cause = plainDispatchFailureCause(failed && failed.result, paused);
3407
+ log(` fable handoff failed: ${cause}. receipt: ${path.relative(root, receiptPath)}`);
3408
+ }
3346
3409
  const completedLabel = reviewOnly ? `${flight.ready.length} proof ready` : `${flight.landed.length} landed`;
3347
3410
  log(` dispatch over: ${completedLabel}, ${flight.paused.length} paused - receipt: ${path.relative(root, flight.receipt)}`);
3348
3411
  log('');
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const knownCommands = ['init', 'log', 'logs', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '_start', 'plan', 'do', 'review', 'release',
3
+ const knownCommands = ['init', 'log', 'logs', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '_start', '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', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
6
6
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', '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', 'sync-checkout',
package/lib/task-db.js CHANGED
@@ -369,8 +369,8 @@ function withTaskDisplayRefs(rows, refRows = rows) {
369
369
  const ids = sorted.map(row => row && row.id);
370
370
  sorted.forEach((row, index) => {
371
371
  refs.set(row.id, {
372
- display_id: taskDisplayRef(row, index),
373
- legacy_ref: shortestUniqueTaskRef(row.id, ids, 8),
372
+ display_id: row.display_id || taskDisplayRef(row, index),
373
+ legacy_ref: row.legacy_ref || shortestUniqueTaskRef(row.id, ids, 8),
374
374
  });
375
375
  });
376
376
  }