atris 3.47.0 → 3.48.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/atris/CLAUDE.md +2 -0
- package/atris/skills/youtube/SKILL.md +6 -4
- package/bin/atris.js +8 -1
- package/commands/dream.js +1 -1
- package/commands/engine.js +12 -5
- package/commands/member.js +38 -1
- package/commands/mission.js +1 -22
- package/commands/task.js +94 -22
- package/commands/who.js +176 -0
- package/commands/worktree.js +7 -5
- package/commands/youtube.js +31 -1
- package/lib/engine-ask.js +15 -1
- package/lib/engine-registry.js +23 -0
- package/lib/fleet.js +123 -45
- package/lib/known-commands.js +1 -1
- package/lib/task-db.js +2 -2
- package/lib/workforce-presence.js +448 -0
- package/package.json +2 -1
- package/scripts/det/README.md +162 -0
- package/scripts/det/ax-lane-eval.js +116 -0
- package/scripts/det/changelog.js +148 -0
- package/scripts/det/codex-watchdog.js +217 -0
- package/scripts/det/commit-msg.js +153 -0
- package/scripts/det/data/ax-lane-gold.jsonl +81 -0
- package/scripts/det/data/ax-lane-holdout.jsonl +20 -0
- package/scripts/det/date.js +91 -0
- package/scripts/det/det.js +149 -0
- package/scripts/det/extract.js +93 -0
- package/scripts/det/hash.js +79 -0
- package/scripts/det/hunk-filter.js +73 -0
- package/scripts/det/json.js +120 -0
- package/scripts/det/pr-description.js +213 -0
- package/scripts/det/test.js +296 -0
- package/scripts/det/text.js +102 -0
- package/scripts/det/voice.js +76 -0
- package/scripts/det/ytnotes +196 -0
- package/scripts/det/ytquote-repair.js +181 -0
- package/scripts/det/ytrail-eval.js +124 -0
package/commands/worktree.js
CHANGED
|
@@ -160,6 +160,8 @@ function listWorktrees(root = repoRoot()) {
|
|
|
160
160
|
// Duplicate flight guard. On 2026-08-07 two agents were dispatched onto the
|
|
161
161
|
// same map rewrite 44 seconds apart: fleet dispatch claims a task first, but a
|
|
162
162
|
// direct `worktree start` had no pre-check beyond the target path existing.
|
|
163
|
+
// Refuse only the same task slug. A shared word on an unrelated flight is not
|
|
164
|
+
// a collision (dispatch-cli-900 must not block dispatch-cli-901).
|
|
163
165
|
const AGENT_FLIGHT_NAME_PATTERN = /^codex\/(.+)-(\d{8}-\d{6})$/;
|
|
164
166
|
const FLIGHT_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
165
167
|
const FLIGHT_STOPWORDS = new Set([
|
|
@@ -234,10 +236,10 @@ function inFlightAgentFlights({ root = repoRoot(), now = new Date(), windowMs =
|
|
|
234
236
|
return [...flights.values()].sort((a, b) => b.stampMs - a.stampMs);
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
function collidingFlights(flights,
|
|
238
|
-
const wanted =
|
|
239
|
-
if (!wanted
|
|
240
|
-
return flights.filter((flight) => flight.
|
|
239
|
+
function collidingFlights(flights, taskSlug) {
|
|
240
|
+
const wanted = String(taskSlug || '').trim().toLowerCase();
|
|
241
|
+
if (!wanted) return [];
|
|
242
|
+
return flights.filter((flight) => String(flight.taskSlug || '').toLowerCase() === wanted);
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
function describeFlightAge(flight, nowMs = Date.now()) {
|
|
@@ -511,7 +513,7 @@ function startWorktree(args) {
|
|
|
511
513
|
const active = flights.filter((flight) => flight.kind === 'worktree');
|
|
512
514
|
const repoName = path.basename(findPrimaryRoot(root));
|
|
513
515
|
console.log(`flights: ${active.length} active agent ${active.length === 1 ? 'worktree' : 'worktrees'} for ${repoName}`);
|
|
514
|
-
const collisions = collidingFlights(flights,
|
|
516
|
+
const collisions = collidingFlights(flights, slugify(task, 'task', 36));
|
|
515
517
|
if (collisions.length) {
|
|
516
518
|
const label = force ? 'warning' : 'refusing';
|
|
517
519
|
for (const flight of collisions) {
|
package/commands/youtube.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
const { apiRequestJson } = require('../utils/api');
|
|
2
2
|
const { ensureValidCredentials } = require('../utils/auth');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
const https = require('https');
|
|
5
6
|
|
|
7
|
+
const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [haiku|atris-fast|gemini|grok|codex|cursor]';
|
|
8
|
+
const YTNOTES_HINT = 'zero credits, local captions + a fast engine';
|
|
9
|
+
|
|
6
10
|
const DEFAULT_QUERY = [
|
|
7
11
|
'Create a timestamped YouTube brief for Atris.',
|
|
8
12
|
'Include: metadata, timestamped outline, core claims with confidence, memorable examples, actionable takeaways, Atris/product implications, and next actions.',
|
|
@@ -19,9 +23,11 @@ const ALLOWED_CAPTION_HOST_SUFFIXES = [
|
|
|
19
23
|
|
|
20
24
|
function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
21
25
|
output('');
|
|
22
|
-
output(`Usage: ${commandName}
|
|
26
|
+
output(`Usage: ${commandName} notes <youtube-url> [engine]`);
|
|
27
|
+
output(` ${commandName} process <youtube-url> [options]`);
|
|
23
28
|
output(` ${commandName} <youtube-url> [options]`);
|
|
24
29
|
output('');
|
|
30
|
+
output('notes = free local notes, process = 5 credits cloud knowledge');
|
|
25
31
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
26
32
|
output('Falls back to cloud video processing when local captions are unavailable.');
|
|
27
33
|
output('');
|
|
@@ -37,6 +43,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
37
43
|
output(' metadata -> timestamped outline -> claims -> examples -> takeaways -> Atris implications -> next actions');
|
|
38
44
|
output('');
|
|
39
45
|
output('Examples:');
|
|
46
|
+
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
40
47
|
output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
41
48
|
output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
|
|
42
49
|
output('');
|
|
@@ -437,8 +444,31 @@ function formatYoutubeResult(data) {
|
|
|
437
444
|
return lines.join('\n');
|
|
438
445
|
}
|
|
439
446
|
|
|
447
|
+
function runYoutubeNotes(args = [], deps = {}) {
|
|
448
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
449
|
+
const url = args[0];
|
|
450
|
+
const engine = args[1];
|
|
451
|
+
if (!url) {
|
|
452
|
+
output(YTNOTES_USAGE);
|
|
453
|
+
output(YTNOTES_HINT);
|
|
454
|
+
return 2;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const script = path.join(__dirname, '..', 'scripts', 'det', 'ytnotes');
|
|
458
|
+
const spawn = deps.spawnSync || spawnSync;
|
|
459
|
+
const childArgs = engine ? [url, engine] : [url];
|
|
460
|
+
const result = spawn(script, childArgs, { stdio: 'inherit' });
|
|
461
|
+
if (result.status == null) return 1;
|
|
462
|
+
return result.status;
|
|
463
|
+
}
|
|
464
|
+
|
|
440
465
|
async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
441
466
|
const output = deps.output || ((line = '') => console.log(line));
|
|
467
|
+
if (argv[0] === 'notes') {
|
|
468
|
+
const code = runYoutubeNotes(argv.slice(1), deps);
|
|
469
|
+
if (!deps.output && !deps.spawnSync) process.exit(code);
|
|
470
|
+
return code;
|
|
471
|
+
}
|
|
442
472
|
const options = parseYoutubeArgs(argv);
|
|
443
473
|
if (options.help) {
|
|
444
474
|
showYoutubeHelp(output, deps.commandName || 'atris youtube');
|
package/lib/engine-ask.js
CHANGED
|
@@ -9,7 +9,11 @@ const {
|
|
|
9
9
|
DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
10
10
|
RUNNER_PROFILE_DEFS,
|
|
11
11
|
} = require('./runner-command');
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
canonicalEngineName,
|
|
14
|
+
engineFailureHealthStatus,
|
|
15
|
+
setEngineHealth,
|
|
16
|
+
} = require('./engine-registry');
|
|
13
17
|
const {
|
|
14
18
|
appendEngineLiveLogChunk,
|
|
15
19
|
createEngineLiveLog,
|
|
@@ -452,6 +456,15 @@ function answerStatus(answer) {
|
|
|
452
456
|
return engineTerminalStatus(answer);
|
|
453
457
|
}
|
|
454
458
|
|
|
459
|
+
function recordEngineAskHealth(answers, root) {
|
|
460
|
+
for (const answer of answers) {
|
|
461
|
+
const status = answer.ok
|
|
462
|
+
? 'ready'
|
|
463
|
+
: engineFailureHealthStatus({ ...answer, status: 'errored' });
|
|
464
|
+
if (status) setEngineHealth(answer.engine, status, root);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
455
468
|
function engineAskReceipt(answers, { concurrency, timeoutMs, at = new Date().toISOString() }) {
|
|
456
469
|
const receiptAnswers = answers.map((answer) => ({ ...answer, status: answerStatus(answer) }));
|
|
457
470
|
const answered = receiptAnswers.filter((answer) => answer.status === 'answered').length;
|
|
@@ -582,6 +595,7 @@ async function runEngineAskCommand(args, root = process.cwd(), deps = {}) {
|
|
|
582
595
|
signal: abort.signal,
|
|
583
596
|
onOutputChunk: (chunk, stream) => appendLiveLog(liveLogPath, chunk, stream),
|
|
584
597
|
});
|
|
598
|
+
recordEngineAskHealth(answers, root);
|
|
585
599
|
const receipt = engineAskReceipt(answers, {
|
|
586
600
|
concurrency: parsed.concurrency,
|
|
587
601
|
timeoutMs: parsed.timeoutMs,
|
package/lib/engine-registry.js
CHANGED
|
@@ -16,6 +16,28 @@ const ENGINE_ROLES = Object.freeze(['navigator', 'executor', 'validator']);
|
|
|
16
16
|
const ENGINE_DUTIES = Object.freeze(['leader', 'errands', 'learning']);
|
|
17
17
|
const ENGINE_HEALTH_STATUSES = Object.freeze(['ready', 'not_installed', 'credit_out', 'error']);
|
|
18
18
|
|
|
19
|
+
function engineFailureHealthStatus(result) {
|
|
20
|
+
if (!result || result.status !== 'errored') return null;
|
|
21
|
+
const signalText = [
|
|
22
|
+
result.reason,
|
|
23
|
+
result.model_unavailable,
|
|
24
|
+
result.report,
|
|
25
|
+
result.stdout,
|
|
26
|
+
result.stderr,
|
|
27
|
+
result.error,
|
|
28
|
+
result.claude && result.claude.summary,
|
|
29
|
+
result.claude && result.claude.receipt_text,
|
|
30
|
+
result.claude && result.claude.stderr,
|
|
31
|
+
result.rate_limit_info && JSON.stringify(result.rate_limit_info),
|
|
32
|
+
].filter(Boolean).join('\n').toLowerCase();
|
|
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
|
+
return 'credit_out';
|
|
35
|
+
}
|
|
36
|
+
if (/not installed|command not found|\benoent\b/.test(signalText)) return 'not_installed';
|
|
37
|
+
if (/timeout|model-unavailable/.test(signalText)) return 'not_installed';
|
|
38
|
+
return 'error';
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
const ENGINE_SEED_META = Object.freeze({
|
|
20
42
|
'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), models: Object.freeze(['atris fast']), duty: 'learning', fallback_order: 10 }),
|
|
21
43
|
codex: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['codex']), fallback_order: 10 }),
|
|
@@ -356,6 +378,7 @@ module.exports = {
|
|
|
356
378
|
resolveEngineForRoleRanked,
|
|
357
379
|
resolveEngineForRole,
|
|
358
380
|
resolveEngineForRoleWithPreference,
|
|
381
|
+
engineFailureHealthStatus,
|
|
359
382
|
setEngineOverrides,
|
|
360
383
|
setEngineHealth,
|
|
361
384
|
};
|
package/lib/fleet.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
worktreeBaseRef,
|
|
24
24
|
} = require('./brief-ledger');
|
|
25
25
|
const { RUNNER_PROFILE_DEFS, buildRunnerCommand } = require('./runner-command');
|
|
26
|
+
const { engineFailureHealthStatus, setEngineHealth } = require('./engine-registry');
|
|
26
27
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
27
28
|
const { rankEnginesDetailed } = require('./router-brain');
|
|
28
29
|
const {
|
|
@@ -207,7 +208,7 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
|
|
|
207
208
|
if (sealed && engineName === 'codex') {
|
|
208
209
|
cmd = cmd.replace(/\bexec\b/, 'exec --sandbox workspace-write --ephemeral --ignore-user-config --ignore-rules');
|
|
209
210
|
}
|
|
210
|
-
if (sealed && engineName === 'claude') {
|
|
211
|
+
if (sealed && (engineName === 'claude' || engineName === 'fable')) {
|
|
211
212
|
cmd = `${cmd} --safe-mode --no-session-persistence --permission-mode acceptEdits --settings '${JSON.stringify({ sandbox: { enabled: true, autoAllowBashIfSandboxed: true } })}'`;
|
|
212
213
|
}
|
|
213
214
|
if (sealed && engineName === 'cursor') cmd = `${cmd} --sandbox enabled`;
|
|
@@ -217,7 +218,7 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
|
|
|
217
218
|
}
|
|
218
219
|
if (engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --permission-mode dangerous ');
|
|
219
220
|
if (yolo && engineName === 'codex') cmd = cmd.replace(/\bexec\b/, `exec ${YOLO_ENGINE_FLAGS.codex}`);
|
|
220
|
-
if (yolo && engineName === 'claude') cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
|
|
221
|
+
if (yolo && (engineName === 'claude' || engineName === 'fable')) cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
|
|
221
222
|
if (engineName === 'codex') cmd = wrapCodexWithWatchdog(cmd, watchdogPath, watchdogReceiptPath);
|
|
222
223
|
return cmd;
|
|
223
224
|
} finally {
|
|
@@ -560,6 +561,37 @@ function detectDeadEngineDispatch(result) {
|
|
|
560
561
|
return { reason: 'nonzero_exit', exitCode };
|
|
561
562
|
}
|
|
562
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
|
+
|
|
583
|
+
function recordDispatchEngineHealth(result, failure, root) {
|
|
584
|
+
if (!result || !result.engine) return null;
|
|
585
|
+
const status = failure
|
|
586
|
+
? engineFailureHealthStatus({
|
|
587
|
+
...result,
|
|
588
|
+
status: 'errored',
|
|
589
|
+
reason: [result.reason, failure.reason].filter(Boolean).join('\n'),
|
|
590
|
+
})
|
|
591
|
+
: 'ready';
|
|
592
|
+
return status ? setEngineHealth(result.engine, status, root) : null;
|
|
593
|
+
}
|
|
594
|
+
|
|
563
595
|
function normalizeInstalledEngines(engines) {
|
|
564
596
|
return [...new Set((engines || [])
|
|
565
597
|
.map((entry) => (typeof entry === 'string' ? entry : entry && entry.name))
|
|
@@ -672,6 +704,7 @@ async function dispatchEntryWithRestaff({
|
|
|
672
704
|
|
|
673
705
|
const first = await runOnce(engine);
|
|
674
706
|
const deadEngine = detectDeadEngineDispatch(first);
|
|
707
|
+
recordDispatchEngineHealth(first, deadEngine, root);
|
|
675
708
|
if (!deadEngine) return first;
|
|
676
709
|
stampDispatchBrief(root, first.brief_id, 'fail', `restaffed from ${engine}: ${deadEngine.reason}`);
|
|
677
710
|
|
|
@@ -687,6 +720,7 @@ async function dispatchEntryWithRestaff({
|
|
|
687
720
|
|
|
688
721
|
restaffState.used = true;
|
|
689
722
|
const fallbackResult = await runOnce(fallback);
|
|
723
|
+
recordDispatchEngineHealth(fallbackResult, detectDeadEngineDispatch(fallbackResult), root);
|
|
690
724
|
return {
|
|
691
725
|
...fallbackResult,
|
|
692
726
|
restaffed: {
|
|
@@ -1066,6 +1100,7 @@ module.exports = {
|
|
|
1066
1100
|
shipWithRetry,
|
|
1067
1101
|
shipFailureDetail,
|
|
1068
1102
|
get FLEET_CAPABLE() { return FLEET_CAPABLE; },
|
|
1103
|
+
get DISPATCH_CAPABLE() { return DISPATCH_CAPABLE; },
|
|
1069
1104
|
get runFleetFlight() { return runFleetFlight; },
|
|
1070
1105
|
get focusedCheck() { return focusedCheck; },
|
|
1071
1106
|
get dispatchCheck() { return dispatchCheck; },
|
|
@@ -1107,6 +1142,7 @@ module.exports = {
|
|
|
1107
1142
|
// Engines that can edit a repo headlessly. atris-fast (ax) is a chat lane,
|
|
1108
1143
|
// not a repo worker — it keeps owning normal mission ticks, not fleet builds.
|
|
1109
1144
|
const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin', 'grok'];
|
|
1145
|
+
const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable'];
|
|
1110
1146
|
|
|
1111
1147
|
let receiptSequence = 0;
|
|
1112
1148
|
function nowStamp() {
|
|
@@ -2643,6 +2679,18 @@ async function runFleetFlight({
|
|
|
2643
2679
|
// ---------------------------------------------------------------------------
|
|
2644
2680
|
// T5 — one-command dispatch: `atris engine dispatch <task-id> --engine <name>`
|
|
2645
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
|
+
|
|
2646
2694
|
// The manual version of this loop took 6 Bash calls per task the night this
|
|
2647
2695
|
// was written: claim, worktree start, prompt file, engine -p, verify, ship.
|
|
2648
2696
|
// One or more explicit task ids build in parallel isolated worktrees on ONE
|
|
@@ -2678,8 +2726,8 @@ async function runDispatchFlight({
|
|
|
2678
2726
|
scoutAsk = null,
|
|
2679
2727
|
} = {}) {
|
|
2680
2728
|
if (!engine) throw new Error('runDispatchFlight: engine is required');
|
|
2681
|
-
if (!
|
|
2682
|
-
throw new Error(`runDispatchFlight: engine "${engine}" cannot build headlessly (capable: ${
|
|
2729
|
+
if (!DISPATCH_CAPABLE.includes(engine)) {
|
|
2730
|
+
throw new Error(`runDispatchFlight: engine "${engine}" cannot build headlessly (capable: ${DISPATCH_CAPABLE.join(', ')})`);
|
|
2683
2731
|
}
|
|
2684
2732
|
const ids = [...new Set((taskIds || []).map((id) => String(id).trim()).filter(Boolean))];
|
|
2685
2733
|
if (!ids.length) throw new Error('runDispatchFlight: at least one task id is required');
|
|
@@ -2745,51 +2793,71 @@ async function runDispatchFlight({
|
|
|
2745
2793
|
log(` x ${taskId} claim failed`);
|
|
2746
2794
|
continue;
|
|
2747
2795
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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`);
|
|
2758
2829
|
continue;
|
|
2759
2830
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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 } : {}),
|
|
2768
2850
|
});
|
|
2769
|
-
log(`
|
|
2770
|
-
|
|
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`);
|
|
2771
2860
|
}
|
|
2772
|
-
const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
|
|
2773
|
-
const startCommit = yolo ? readStartCommit({ worktreePath }) : '';
|
|
2774
|
-
const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
|
|
2775
|
-
const safetyPrompt = reviewOnly
|
|
2776
|
-
? `${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.`
|
|
2777
|
-
: basePrompt;
|
|
2778
|
-
const trustedPrompt = trustedVerifier
|
|
2779
|
-
? `${safetyPrompt}\n\nTrusted verifier (run it bare before reporting done): ${trustedVerifier}`
|
|
2780
|
-
: promptOverride;
|
|
2781
|
-
prepared.push({
|
|
2782
|
-
task,
|
|
2783
|
-
taskId,
|
|
2784
|
-
worktreePath,
|
|
2785
|
-
landingWorktreePath,
|
|
2786
|
-
engine,
|
|
2787
|
-
remoteBoundary,
|
|
2788
|
-
remoteMasterBefore: remoteBoundary ? remoteBoundary.protectedMaster : '',
|
|
2789
|
-
startCommit,
|
|
2790
|
-
...(trustedPrompt ? { prompt: trustedPrompt } : {}),
|
|
2791
|
-
});
|
|
2792
|
-
log(` building ${taskId} in ${path.basename(worktreePath)}`);
|
|
2793
2861
|
}
|
|
2794
2862
|
|
|
2795
2863
|
const dispatch = dispatcher || ((entry) => new Promise((resolve) => {
|
|
@@ -2813,6 +2881,10 @@ async function runDispatchFlight({
|
|
|
2813
2881
|
}));
|
|
2814
2882
|
const restaffState = { used: false };
|
|
2815
2883
|
|
|
2884
|
+
if (engine === 'fable' && prepared.length) {
|
|
2885
|
+
log(` fable handoff started: receipt ${path.relative(root, receiptPath)}`);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2816
2888
|
const results = await Promise.all(prepared.map((entry) => {
|
|
2817
2889
|
const startedAtMs = Date.now();
|
|
2818
2890
|
return dispatchEntryWithRestaff({
|
|
@@ -3328,6 +3400,12 @@ async function runDispatchFlight({
|
|
|
3328
3400
|
flight.finished_at = new Date().toISOString();
|
|
3329
3401
|
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
3330
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
|
+
}
|
|
3331
3409
|
const completedLabel = reviewOnly ? `${flight.ready.length} proof ready` : `${flight.landed.length} landed`;
|
|
3332
3410
|
log(` dispatch over: ${completedLabel}, ${flight.paused.length} paused - receipt: ${path.relative(root, flight.receipt)}`);
|
|
3333
3411
|
log('');
|
package/lib/known-commands.js
CHANGED
|
@@ -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
|
}
|