atris 3.42.0 → 3.44.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.
Files changed (45) hide show
  1. package/atris/skills/design/SKILL.md +7 -1
  2. package/atris/skills/engines/SKILL.md +44 -13
  3. package/atris/team/customer-lead/MEMBER.md +45 -0
  4. package/atris/team/customer-lead/SOUL.md +33 -0
  5. package/atris/team/customer-lead/START_HERE.md +7 -0
  6. package/atris/team/customer-lead/skills/customer-commitments/SKILL.md +45 -0
  7. package/atris/team/improver/MEMBER.md +33 -0
  8. package/bin/atris.js +37 -4
  9. package/commands/autoland.js +15 -1
  10. package/commands/caretaker.js +303 -0
  11. package/commands/clean.js +76 -0
  12. package/commands/engine-watch.js +212 -0
  13. package/commands/engine.js +99 -11
  14. package/commands/founder.js +304 -0
  15. package/commands/human-missions.js +844 -0
  16. package/commands/init.js +16 -7
  17. package/commands/lesson.js +178 -4
  18. package/commands/mission.js +124 -69
  19. package/commands/slop.js +34 -3
  20. package/commands/task.js +51 -4
  21. package/commands/team.js +329 -13
  22. package/commands/verify.js +99 -6
  23. package/commands/worktree.js +119 -4
  24. package/lib/auto-accept-certified.js +302 -0
  25. package/lib/cloud-mission.js +59 -2
  26. package/lib/conductor-artifacts.js +1 -1
  27. package/lib/dispatch-scout.js +383 -0
  28. package/lib/engine-ask.js +645 -0
  29. package/lib/engine-job-lifecycle.js +65 -0
  30. package/lib/engine-receipt-sweep.js +98 -0
  31. package/lib/engine-registry.js +2 -2
  32. package/lib/engine-validate.js +374 -0
  33. package/lib/fleet.js +459 -106
  34. package/lib/known-commands.js +2 -2
  35. package/lib/lesson-ledger.js +84 -0
  36. package/lib/member-alive.js +2 -2
  37. package/lib/policy-lessons.js +70 -0
  38. package/lib/receipt-evidence.js +56 -1
  39. package/lib/runner-command.js +1 -1
  40. package/lib/secret-gateway.js +588 -0
  41. package/lib/team-presence.js +13 -1
  42. package/lib/voice-gate.js +6 -0
  43. package/lib/wish-audit.js +5 -205
  44. package/lib/wish-delegate.js +5 -2
  45. package/package.json +6 -1
package/lib/fleet.js CHANGED
@@ -15,7 +15,7 @@ const fs = require('fs');
15
15
  const crypto = require('crypto');
16
16
  const os = require('os');
17
17
  const path = require('path');
18
- const { spawnSync } = require('child_process');
18
+ const { spawn, spawnSync } = require('child_process');
19
19
  const {
20
20
  appendBriefRecord,
21
21
  mirrorBriefRecord,
@@ -33,6 +33,14 @@ const { listWorktrees } = require('../commands/worktree');
33
33
  const { isConductorStatusLine } = require('./conductor-artifacts');
34
34
  const { matchLessons } = require('./lesson-preflight');
35
35
  const { matchTaste } = require('./taste-lessons');
36
+ const { buildVerifiedScoutPack, appendVerifiedScoutPack } = require('./dispatch-scout');
37
+ const {
38
+ applySecretGrantEnvironment,
39
+ buildGatewaySupervisorScript,
40
+ spawnBlocking,
41
+ PROXY_ENV_KEYS: SECRET_GATEWAY_PROXY_ENV_KEYS,
42
+ } = require('./secret-gateway');
43
+ const { appendEngineLiveLogChunk, createEngineLiveLog, engineTerminalReason } = require('./engine-job-lifecycle');
36
44
 
37
45
  // Lanes a fleet may never staff on its own: the human keeps irreversible
38
46
  // calls. Mirrors the autoland denied lanes.
@@ -66,6 +74,15 @@ const METHOD_KERNEL = [
66
74
  'Smallest diff that satisfies Done wins; prefer deleting code over adding it.',
67
75
  ];
68
76
 
77
+ const ATRIS_BUILD_PROCESS_PREAMBLE = [
78
+ 'you are set up to do this well.',
79
+ 'use the atris process:',
80
+ 'claim the task, read the map, and work only the named contract.',
81
+ 'run every gate bare and read its real exit code.',
82
+ 'report honestly, including failures, and stop at the named stop point.',
83
+ 'never use --no-verify.',
84
+ ].join('\n');
85
+
69
86
  // The bounded prompt every engine gets. Same contract the manual flight used:
70
87
  // isolated worktree, commit never push, MAP first, focused verify, report.
71
88
  function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
@@ -79,6 +96,8 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
79
96
  const lines = [
80
97
  'First, run `atris worktree guard`; if it fails, stop immediately, report back, and do not edit anything. Do this before any file edit.',
81
98
  '',
99
+ ATRIS_BUILD_PROCESS_PREAMBLE,
100
+ '',
82
101
  `You are working task ${ref} in this repo checkout (an isolated git worktree${worktreePath ? ` at ${worktreePath}` : ''} — commit here, NEVER push).`,
83
102
  '',
84
103
  `Task: ${title}`,
@@ -141,6 +160,9 @@ const YOLO_ENGINE_FLAGS = Object.freeze({
141
160
  claude: '--dangerously-skip-permissions',
142
161
  });
143
162
  const DISPATCH_SELF_LAND_TARGET = 'origin/master';
163
+ const DEFAULT_DISPATCH_TIMEOUT_MS = 900000;
164
+ const CODEX_DISPATCH_TIMEOUT_MS = 3660000;
165
+ const CODEX_WATCHDOG_SOURCE = path.join(__dirname, '..', 'scripts', 'det', 'codex-watchdog.js');
144
166
 
145
167
  function realpathOrResolve(value) {
146
168
  const resolved = path.resolve(String(value));
@@ -164,7 +186,16 @@ function assertIsolatedWorktree(worktreePath, root = process.cwd()) {
164
186
  return { worktreePath: resolvedWorktree, primaryRoot };
165
187
  }
166
188
 
167
- function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = false, allowedTools = FLEET_ALLOWED_TOOLS } = {}) {
189
+ function wrapCodexWithWatchdog(cmd, watchdogPath = CODEX_WATCHDOG_SOURCE, receiptPath = '') {
190
+ // Codex hangs forever at startup when stdin is an open pipe (three real
191
+ // flights on 2026-08-11 sat 50-100 minutes at zero CPU). Force every fleet
192
+ // codex spawn through the silent-start / runtime-cap watchdog with stdin
193
+ // sealed, so orchestrator prompts cannot forget the convention.
194
+ const receiptArg = receiptPath ? ` --receipt ${shellSingleQuote(receiptPath)}` : '';
195
+ return `${shellSingleQuote(process.execPath)} ${shellSingleQuote(watchdogPath)} --startup-deadline 90 --max-runtime 3600${receiptArg} -- sh -c ${shellSingleQuote(cmd)} </dev/null`;
196
+ }
197
+
198
+ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = false, allowedTools = FLEET_ALLOWED_TOOLS, watchdogPath = CODEX_WATCHDOG_SOURCE, watchdogReceiptPath = '' } = {}) {
168
199
  if (!RUNNER_PROFILE_DEFS[engineName]) throw new Error(`unknown engine "${engineName}"`);
169
200
  const prev = process.env.ATRIS_RUNNER_PROFILE;
170
201
  process.env.ATRIS_RUNNER_PROFILE = engineName;
@@ -187,6 +218,7 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
187
218
  if (engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --permission-mode dangerous ');
188
219
  if (yolo && engineName === 'codex') cmd = cmd.replace(/\bexec\b/, `exec ${YOLO_ENGINE_FLAGS.codex}`);
189
220
  if (yolo && engineName === 'claude') cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
221
+ if (engineName === 'codex') cmd = wrapCodexWithWatchdog(cmd, watchdogPath, watchdogReceiptPath);
190
222
  return cmd;
191
223
  } finally {
192
224
  if (prev === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
@@ -246,65 +278,173 @@ function terminateSameSandboxProfile(executable, args, options) {
246
278
  });
247
279
  }
248
280
 
281
+ function spawnWithOutputChunks(executable, args, options = {}) {
282
+ const {
283
+ encoding,
284
+ input,
285
+ onOutputChunk,
286
+ timeout,
287
+ ...childOptions
288
+ } = options;
289
+ const child = spawn(executable, args, {
290
+ ...childOptions,
291
+ stdio: [input == null ? 'ignore' : 'pipe', 'pipe', 'pipe'],
292
+ });
293
+ if (input != null && child.stdin) {
294
+ child.stdin.on('error', () => {});
295
+ child.stdin.end(input);
296
+ }
297
+
298
+ const stdoutChunks = [];
299
+ const stderrChunks = [];
300
+ let spawnError = null;
301
+ let timeoutError = null;
302
+ const capture = (chunks, stream) => (chunk) => {
303
+ chunks.push(Buffer.from(chunk));
304
+ if (typeof onOutputChunk === 'function') onOutputChunk(chunk, stream);
305
+ };
306
+ child.stdout.on('data', capture(stdoutChunks, 'stdout'));
307
+ child.stderr.on('data', capture(stderrChunks, 'stderr'));
308
+ child.on('error', (error) => { spawnError = error; });
309
+
310
+ return new Promise((resolve) => {
311
+ const timer = Number(timeout) > 0
312
+ ? setTimeout(() => {
313
+ timeoutError = new Error('spawn ETIMEDOUT');
314
+ timeoutError.code = 'ETIMEDOUT';
315
+ try { child.kill('SIGKILL'); } catch {}
316
+ }, Number(timeout))
317
+ : null;
318
+ child.once('close', (status, signal) => {
319
+ if (timer) clearTimeout(timer);
320
+ const encode = (chunks) => {
321
+ const output = Buffer.concat(chunks);
322
+ return encoding ? output.toString(encoding) : output;
323
+ };
324
+ resolve({
325
+ pid: child.pid,
326
+ status,
327
+ signal,
328
+ stdout: encode(stdoutChunks),
329
+ stderr: encode(stderrChunks),
330
+ error: timeoutError || spawnError,
331
+ });
332
+ });
333
+ });
334
+ }
335
+
249
336
  function runInReapedProcessGroup(executable, args, options, controlDir, statusFile) {
250
337
  if (!controlDir || !statusFile) throw new Error('sealed execution requires isolated control and status paths');
251
338
  const supervisor = path.join(controlDir, 'sandbox-supervisor.js');
252
339
  const stateFile = path.join(controlDir, 'sandbox-process.json');
253
340
  const leaseFile = path.join(controlDir, 'sandbox-process.lease');
254
- fs.writeFileSync(supervisor, [
255
- "'use strict';",
256
- "const fs = require('node:fs');",
257
- "const { spawn, spawnSync } = require('node:child_process');",
258
- "const [stateFile, leaseFile, statusFile, executable, ...args] = process.argv.slice(2);",
259
- "const leaseFd = fs.openSync(leaseFile, 'w', 0o600);",
260
- "const statusFd = fs.openSync(statusFile, 'w', 0o600);",
261
- "const child = spawn(executable, args, { cwd: process.cwd(), env: process.env, detached: true, stdio: ['ignore', 'inherit', 'inherit', leaseFd, statusFd] });",
262
- "fs.closeSync(leaseFd);",
263
- "fs.closeSync(statusFd);",
264
- "fs.writeFileSync(stateFile, JSON.stringify({ pgid: child.pid, cwd: process.cwd() }) + '\\n', { mode: 0o600 });",
265
- "let stopping = false;",
266
- "const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
267
- "function trackedPids() {",
268
- " const pids = new Set();",
269
- " for (const [bin, argv] of [['/usr/bin/pgrep', ['-g', String(child.pid)]], ['/usr/sbin/lsof', ['-t', leaseFile]], ['/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', process.cwd(), '-t']]]) {",
270
- " const found = spawnSync(bin, argv, { encoding: 'utf8' });",
271
- " for (const value of String(found.stdout || '').trim().split(/\\s+/)) {",
272
- " const pid = Number(value);",
273
- " if (Number.isInteger(pid) && pid > 0) pids.add(pid);",
274
- " }",
275
- " }",
276
- " pids.delete(process.pid);",
277
- " return [...pids];",
278
- "}",
279
- "async function stop(code) {",
280
- " if (stopping) return;",
281
- " stopping = true;",
282
- " if (executable === '/usr/bin/sandbox-exec' && args[0] === '-p' && args[1]) {",
283
- " spawnSync(executable, ['-p', args[1], '/bin/kill', '-KILL', '-1'], { cwd: process.cwd(), env: process.env, stdio: 'ignore', timeout: 5000 });",
284
- " }",
285
- " for (const [signal, delay] of [['SIGTERM', 100], ['SIGKILL', 100], ['SIGKILL', 100]]) {",
286
- " try { process.kill(-child.pid, signal); } catch {}",
287
- " for (const pid of trackedPids()) { try { process.kill(pid, signal); } catch {} }",
288
- " await wait(delay);",
289
- " }",
290
- " let exitCode = Number.isInteger(code) ? code : 128;",
291
- " try {",
292
- " const savedText = fs.readFileSync(statusFile, 'utf8').trim();",
293
- " const saved = Number(savedText);",
294
- " if (savedText && Number.isInteger(saved) && saved >= 0 && saved <= 255) exitCode = saved;",
295
- " } catch {}",
296
- " process.exit(exitCode);",
297
- "}",
298
- "child.once('error', () => { void stop(1); });",
299
- "child.once('exit', (code) => { void stop(code); });",
300
- "for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => { void stop(143); });",
301
- '',
302
- ].join('\n'), { mode: 0o700 });
341
+ const { onOutputChunk, ...baseSpawnOptions } = options;
342
+ const spawnEnv = { ...(baseSpawnOptions.env || {}) };
343
+ const gatewayPlanRaw = spawnEnv.ATRIS_ONE_LAP_SECRET_GATEWAY;
344
+ let gatewayInput = null;
345
+ if (gatewayPlanRaw) {
346
+ let plan;
347
+ try { plan = JSON.parse(gatewayPlanRaw); } catch {
348
+ throw new Error('secret gateway plan is not valid json');
349
+ }
350
+ const secretEnv = String(plan && plan.grant && plan.grant.secretEnv || '');
351
+ const secret = String(process.env[secretEnv] || '');
352
+ if (!secretEnv || !secret) throw new Error('secret grant requires the parent secret value on stdin path');
353
+ gatewayInput = JSON.stringify({
354
+ grant: plan.grant,
355
+ placeholder: plan.placeholder,
356
+ secret,
357
+ upstreamPort: plan.upstreamPort,
358
+ upstreamAddress: plan.upstreamAddress,
359
+ rejectUnauthorized: plan.rejectUnauthorized,
360
+ });
361
+ delete spawnEnv.ATRIS_ONE_LAP_SECRET_GATEWAY;
362
+ spawnEnv.ATRIS_ONE_LAP_SECRET_GATEWAY_STDIN = '1';
363
+ fs.writeFileSync(supervisor, buildGatewaySupervisorScript(require.resolve('./secret-gateway')), { mode: 0o700 });
364
+ } else {
365
+ fs.writeFileSync(supervisor, [
366
+ "'use strict';",
367
+ "const fs = require('node:fs');",
368
+ "const { spawn, spawnSync } = require('node:child_process');",
369
+ "const [stateFile, leaseFile, statusFile, executable, ...args] = process.argv.slice(2);",
370
+ "const leaseFd = fs.openSync(leaseFile, 'w', 0o600);",
371
+ "const statusFd = fs.openSync(statusFile, 'w', 0o600);",
372
+ "const child = spawn(executable, args, { cwd: process.cwd(), env: process.env, detached: true, stdio: ['ignore', 'inherit', 'inherit', leaseFd, statusFd] });",
373
+ "fs.closeSync(leaseFd);",
374
+ "fs.closeSync(statusFd);",
375
+ "fs.writeFileSync(stateFile, JSON.stringify({ pgid: child.pid, cwd: process.cwd() }) + '\\n', { mode: 0o600 });",
376
+ "let stopping = false;",
377
+ "const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
378
+ "function trackedPids() {",
379
+ " const pids = new Set();",
380
+ " for (const [bin, argv] of [['/usr/bin/pgrep', ['-g', String(child.pid)]], ['/usr/sbin/lsof', ['-t', leaseFile]], ['/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', process.cwd(), '-t']]]) {",
381
+ " const found = spawnSync(bin, argv, { encoding: 'utf8' });",
382
+ " for (const value of String(found.stdout || '').trim().split(/\\s+/)) {",
383
+ " const pid = Number(value);",
384
+ " if (Number.isInteger(pid) && pid > 0) pids.add(pid);",
385
+ " }",
386
+ " }",
387
+ " pids.delete(process.pid);",
388
+ " return [...pids];",
389
+ "}",
390
+ "async function stop(code) {",
391
+ " if (stopping) return;",
392
+ " stopping = true;",
393
+ " if (executable === '/usr/bin/sandbox-exec' && args[0] === '-p' && args[1]) {",
394
+ " spawnSync(executable, ['-p', args[1], '/bin/kill', '-KILL', '-1'], { cwd: process.cwd(), env: process.env, stdio: 'ignore', timeout: 5000 });",
395
+ " }",
396
+ " for (const [signal, delay] of [['SIGTERM', 100], ['SIGKILL', 100], ['SIGKILL', 100]]) {",
397
+ " try { process.kill(-child.pid, signal); } catch {}",
398
+ " for (const pid of trackedPids()) { try { process.kill(pid, signal); } catch {} }",
399
+ " await wait(delay);",
400
+ " }",
401
+ " let exitCode = Number.isInteger(code) ? code : 128;",
402
+ " try {",
403
+ " const savedText = fs.readFileSync(statusFile, 'utf8').trim();",
404
+ " const saved = Number(savedText);",
405
+ " if (savedText && Number.isInteger(saved) && saved >= 0 && saved <= 255) exitCode = saved;",
406
+ " } catch {}",
407
+ " process.exit(exitCode);",
408
+ "}",
409
+ "child.once('error', () => { void stop(1); });",
410
+ "child.once('exit', (code) => { void stop(code); });",
411
+ "for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => { void stop(143); });",
412
+ '',
413
+ ].join('\n'), { mode: 0o700 });
414
+ }
415
+ const spawnOptions = { ...baseSpawnOptions, env: spawnEnv };
416
+ if (gatewayInput) spawnOptions.input = gatewayInput;
417
+ if (gatewayInput) {
418
+ const exitFile = path.join(controlDir, 'sandbox-supervisor.exit');
419
+ return spawnBlocking(
420
+ process.execPath,
421
+ [supervisor, exitFile, stateFile, leaseFile, statusFile, executable, ...args],
422
+ {
423
+ ...spawnOptions,
424
+ exitFile,
425
+ onStdoutChunk: onOutputChunk ? (chunk) => onOutputChunk(chunk, 'stdout') : undefined,
426
+ onStderrChunk: onOutputChunk ? (chunk) => onOutputChunk(chunk, 'stderr') : undefined,
427
+ },
428
+ ).finally(() => {
429
+ terminateSameSandboxProfile(executable, args, spawnOptions);
430
+ terminateTrackedSandbox(stateFile, leaseFile);
431
+ });
432
+ }
433
+ if (onOutputChunk) {
434
+ return spawnWithOutputChunks(
435
+ process.execPath,
436
+ [supervisor, stateFile, leaseFile, statusFile, executable, ...args],
437
+ { ...spawnOptions, onOutputChunk },
438
+ ).finally(() => {
439
+ terminateSameSandboxProfile(executable, args, spawnOptions);
440
+ terminateTrackedSandbox(stateFile, leaseFile);
441
+ });
442
+ }
303
443
  let result;
304
444
  try {
305
- result = spawnSync(process.execPath, [supervisor, stateFile, leaseFile, statusFile, executable, ...args], options);
445
+ result = spawnSync(process.execPath, [supervisor, stateFile, leaseFile, statusFile, executable, ...args], spawnOptions);
306
446
  } finally {
307
- terminateSameSandboxProfile(executable, args, options);
447
+ terminateSameSandboxProfile(executable, args, spawnOptions);
308
448
  terminateTrackedSandbox(stateFile, leaseFile);
309
449
  }
310
450
  return result;
@@ -338,10 +478,14 @@ function dispatchResultOutput(result) {
338
478
  }
339
479
 
340
480
  function dispatchResultExitCode(result) {
341
- if (!result || typeof result !== 'object') return 0;
342
- if (Object.prototype.hasOwnProperty.call(result, 'exitCode')) return result.exitCode;
343
- if (Object.prototype.hasOwnProperty.call(result, 'status')) return result.status;
344
- return 0;
481
+ if (!result || typeof result !== 'object') return null;
482
+ if (Object.prototype.hasOwnProperty.call(result, 'exitCode')) {
483
+ return Number.isInteger(result.exitCode) ? result.exitCode : null;
484
+ }
485
+ if (Object.prototype.hasOwnProperty.call(result, 'status')) {
486
+ return Number.isInteger(result.status) ? result.status : null;
487
+ }
488
+ return null;
345
489
  }
346
490
 
347
491
  // A child killed by a signal comes back either with spawnSync's `signal` field
@@ -400,13 +544,19 @@ function stampDispatchBrief(root, briefId, result, note) {
400
544
  }
401
545
 
402
546
  function detectDeadEngineDispatch(result) {
403
- const exitCode = dispatchResultExitCode(result);
404
- if (exitCode === 0) return null;
405
- const output = dispatchResultOutput(result).toLowerCase();
406
- const pattern = DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => output.includes(p));
407
- if (pattern) return { reason: 'usage_limit', pattern };
547
+ const terminal = dispatchResultToTerminal({}, result);
548
+ const exitCode = terminal.exit_code;
408
549
  const signal = dispatchResultSignal(result);
409
550
  if (signal) return { reason: 'signalled', signal, exitCode };
551
+ if (terminal.reason === 'ok') return null;
552
+ const output = dispatchResultOutput(result).toLowerCase();
553
+ const pattern = Number.isInteger(exitCode) && exitCode !== 0
554
+ ? DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => output.includes(p))
555
+ : null;
556
+ if (pattern) return { reason: 'usage_limit', pattern };
557
+ if (terminal.reason === 'no_output' || terminal.reason === 'unknown' || terminal.reason === 'timeout' || terminal.reason === 'cancelled') {
558
+ return { reason: terminal.reason, exitCode };
559
+ }
410
560
  return { reason: 'nonzero_exit', exitCode };
411
561
  }
412
562
 
@@ -468,6 +618,7 @@ function failedDispatchLeg(result, engineName) {
468
618
  leg.signal = signal;
469
619
  if (!report) leg.report = `(no report: killed by ${signal})`;
470
620
  }
621
+ if (result && result.watchdog_receipt) leg.watchdog_receipt = result.watchdog_receipt;
471
622
  return leg;
472
623
  }
473
624
 
@@ -478,9 +629,23 @@ async function dispatchEntryWithRestaff({
478
629
  dispatch,
479
630
  installedEngines = null,
480
631
  restaffState,
632
+ scoutAsk,
481
633
  }) {
634
+ const basePrompt = entry.prompt || buildFleetPrompt(entry.task, { worktreePath: entry.worktreePath, yolo: entry.yolo });
635
+ let scoutPackPromise = null;
636
+ const scoutPack = () => {
637
+ if (scoutAsk === false) return Promise.resolve(null);
638
+ if (!scoutPackPromise) {
639
+ scoutPackPromise = buildVerifiedScoutPack({
640
+ task: entry.task,
641
+ worktreePath: entry.worktreePath,
642
+ ask: typeof scoutAsk === 'function' ? scoutAsk : undefined,
643
+ });
644
+ }
645
+ return scoutPackPromise;
646
+ };
482
647
  const runOnce = async (engineName) => {
483
- const prompt = entry.prompt || buildFleetPrompt(entry.task, { worktreePath: entry.worktreePath, yolo: entry.yolo });
648
+ const prompt = appendVerifiedScoutPack(basePrompt, await scoutPack(), { worktreePath: entry.worktreePath });
484
649
  const brief = captureDispatchBrief({
485
650
  root,
486
651
  task: entry.task,
@@ -538,7 +703,7 @@ async function dispatchEntryWithRestaff({
538
703
  // injectable for tests. `prompt` is injectable too: a caller-supplied prompt
539
704
  // (e.g. `atris engine dispatch --prompt-file`) skips the generated
540
705
  // buildFleetPrompt text entirely.
541
- function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), timeoutMs = 900000, runner = null, prompt: promptOverride = '', yolo = false, sealed = false, briefId = '', skipBriefCapture = false, environment = null, allowedTools = FLEET_ALLOWED_TOOLS }) {
706
+ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), timeoutMs = null, runner = null, prompt: promptOverride = '', yolo = false, sealed = false, briefId = '', skipBriefCapture = false, environment = null, allowedTools = FLEET_ALLOWED_TOOLS, liveLogPath = '' }) {
542
707
  assertIsolatedWorktree(worktreePath, root);
543
708
  const prompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
544
709
  let capturedBriefId = briefId;
@@ -547,9 +712,28 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
547
712
  }
548
713
  const runtimeDir = String(environment && environment.ATRIS_ONE_LAP_RUNTIME_DIR || '');
549
714
  const promptFile = path.join(sealed && runtimeDir ? runtimeDir : path.join(worktreePath, '.atris'), `fleet-prompt-${task.display_id || 'task'}.md`);
550
- fs.mkdirSync(path.dirname(promptFile), { recursive: true });
715
+ const runtimePath = path.dirname(promptFile);
716
+ fs.mkdirSync(runtimePath, { recursive: true });
551
717
  fs.writeFileSync(promptFile, prompt);
552
- const command = buildEngineCommand(engine, promptFile, { yolo, sealed, allowedTools });
718
+ let watchdogPath = CODEX_WATCHDOG_SOURCE;
719
+ let watchdogArtifact = '';
720
+ if (engine === 'codex') {
721
+ watchdogPath = path.join(runtimePath, 'codex-watchdog.js');
722
+ fs.copyFileSync(CODEX_WATCHDOG_SOURCE, watchdogPath);
723
+ const artifactDir = path.join(worktreePath, '.atris');
724
+ fs.mkdirSync(artifactDir, { recursive: true });
725
+ watchdogArtifact = path.join(artifactDir, `codex-watchdog-${task.display_id || 'task'}-${crypto.randomBytes(4).toString('hex')}.json`);
726
+ }
727
+ const command = buildEngineCommand(engine, promptFile, {
728
+ yolo,
729
+ sealed,
730
+ allowedTools,
731
+ watchdogPath,
732
+ watchdogReceiptPath: watchdogArtifact,
733
+ });
734
+ const dispatchTimeoutMs = timeoutMs === null || timeoutMs === undefined
735
+ ? (engine === 'codex' ? CODEX_DISPATCH_TIMEOUT_MS : DEFAULT_DISPATCH_TIMEOUT_MS)
736
+ : timeoutMs;
553
737
  const exec = runner || ((cmd) => {
554
738
  const childEnv = sealed && environment
555
739
  ? { ...environment }
@@ -564,6 +748,11 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
564
748
  delete childEnv.ATRIS_ONE_LAP_CONTROL_DIR;
565
749
  delete childEnv.ATRIS_ONE_LAP_LIFECYCLE_WRAPPER;
566
750
  delete childEnv.ATRIS_ONE_LAP_STATUS_FILE;
751
+ let deferredCleanup = false;
752
+ const cleanup = () => {
753
+ if (cleanupRuntimeDir) fs.rmSync(cleanupRuntimeDir, { recursive: true, force: true });
754
+ if (cleanupControlDir) fs.rmSync(cleanupControlDir, { recursive: true, force: true });
755
+ };
567
756
  try {
568
757
  const executable = sandboxProfile ? '/usr/bin/sandbox-exec' : 'sh';
569
758
  const args = sandboxProfile
@@ -573,29 +762,49 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
573
762
  cwd: worktreePath,
574
763
  env: childEnv,
575
764
  encoding: 'utf8',
576
- timeout: timeoutMs,
765
+ timeout: dispatchTimeoutMs,
577
766
  };
578
- return sandboxProfile
767
+ if (liveLogPath) {
768
+ fs.mkdirSync(path.dirname(liveLogPath), { recursive: true });
769
+ spawnOptions.onOutputChunk = (chunk) => appendEngineLiveLogChunk(liveLogPath, chunk);
770
+ }
771
+ const run = sandboxProfile
579
772
  ? runInReapedProcessGroup(executable, args, spawnOptions, cleanupControlDir, statusFile)
580
- : spawnSync(executable, args, spawnOptions);
773
+ : (liveLogPath ? spawnWithOutputChunks(executable, args, spawnOptions) : spawnSync(executable, args, spawnOptions));
774
+ if (run && typeof run.then === 'function') {
775
+ deferredCleanup = true;
776
+ return run.finally(cleanup);
777
+ }
778
+ return run;
581
779
  } finally {
582
- if (cleanupRuntimeDir) fs.rmSync(cleanupRuntimeDir, { recursive: true, force: true });
583
- if (cleanupControlDir) fs.rmSync(cleanupControlDir, { recursive: true, force: true });
780
+ if (!deferredCleanup) cleanup();
584
781
  }
585
782
  });
586
- const result = exec(command);
587
- return {
588
- task: task.display_id || task.id,
589
- engine,
590
- worktreePath,
591
- promptFile,
592
- brief_id: capturedBriefId || null,
593
- command,
594
- exitCode: result.status,
595
- signal: result.signal || null,
596
- report: String(result.stdout || '').slice(-8000),
597
- stderr: String(result.stderr || '').slice(-2000),
783
+ const result = exec(command, { timeoutMs: dispatchTimeoutMs });
784
+ const toDispatch = (run) => {
785
+ let watchdogReceipt = null;
786
+ if (watchdogArtifact && fs.existsSync(watchdogArtifact)) {
787
+ try { watchdogReceipt = JSON.parse(fs.readFileSync(watchdogArtifact, 'utf8')); } catch {}
788
+ }
789
+ return {
790
+ task: task.display_id || task.id,
791
+ engine,
792
+ worktreePath,
793
+ promptFile,
794
+ brief_id: capturedBriefId || null,
795
+ command,
796
+ watchdog_artifact: watchdogArtifact || null,
797
+ watchdog_receipt: watchdogReceipt,
798
+ exitCode: Number.isInteger(run.status) ? run.status : null,
799
+ signal: run.signal || null,
800
+ timed_out: Boolean(run.error && run.error.code === 'ETIMEDOUT'),
801
+ cancelled: Boolean(run.cancelled),
802
+ report: String(run.stdout || '').slice(-8000),
803
+ stderr: String(run.stderr || '').slice(-2000),
804
+ };
598
805
  };
806
+ if (result && typeof result.then === 'function') return result.then(toDispatch);
807
+ return toDispatch(result);
599
808
  }
600
809
 
601
810
  // ---------------------------------------------------------------------------
@@ -686,6 +895,21 @@ function clipHeadTail(text, { head = 400, tail = 300 } = {}) {
686
895
  return `${s.slice(0, head)}\n...\n${s.slice(-tail)}`;
687
896
  }
688
897
 
898
+ function dispatchResultToTerminal(entry = {}, result = {}) {
899
+ const payload = {
900
+ engine: result.engine || entry.engine || '',
901
+ task: entry.taskId || (entry.task && entry.task.display_id) || '',
902
+ exit_code: dispatchResultExitCode(result),
903
+ timed_out: Boolean(result.timed_out),
904
+ cancelled: Boolean(result.cancelled),
905
+ stdout: [result.report, result.stdout].map((value) => String(value || '')).join('\n'),
906
+ stderr: result.stderr || '',
907
+ };
908
+ payload.reason = engineTerminalReason(payload);
909
+ payload.ok = payload.reason === 'ok';
910
+ return payload;
911
+ }
912
+
689
913
  // Fleet landings always target master. Without the explicit --target, ship
690
914
  // falls back to the launcher branch's atris-base — a flight launched from a
691
915
  // feature-branch checkout would merge PRs into that branch while the receipt
@@ -838,6 +1062,7 @@ module.exports = {
838
1062
  DENIED_TAGS,
839
1063
  DEAD_ENGINE_OUTPUT_PATTERNS,
840
1064
  reviewOnlyEngineEnvironment,
1065
+ runInReapedProcessGroup,
841
1066
  shipWithRetry,
842
1067
  shipFailureDetail,
843
1068
  get FLEET_CAPABLE() { return FLEET_CAPABLE; },
@@ -850,6 +1075,7 @@ module.exports = {
850
1075
  DISPATCH_SELF_LAND_TARGET,
851
1076
  parseDoneCheck,
852
1077
  METHOD_KERNEL,
1078
+ ATRIS_BUILD_PROCESS_PREAMBLE,
853
1079
  buildFleetPrompt,
854
1080
  assertIsolatedWorktree,
855
1081
  buildEngineCommand,
@@ -867,6 +1093,7 @@ module.exports = {
867
1093
  assignEngines,
868
1094
  landArrival,
869
1095
  detectDeadEngineDispatch,
1096
+ dispatchResultToTerminal,
870
1097
  failedDispatchLeg,
871
1098
  fleetShipArgs,
872
1099
  clipHeadTail,
@@ -1130,7 +1357,7 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
1130
1357
  ATRIS_ONE_LAP_LIFECYCLE_WRAPPER: lifecycle.wrapper,
1131
1358
  ATRIS_ONE_LAP_STATUS_FILE: lifecycle.statusFile,
1132
1359
  };
1133
- for (const key of [
1360
+ const inheritKeys = [
1134
1361
  'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'NO_COLOR', 'USER', 'LOGNAME', 'SHELL',
1135
1362
  'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
1136
1363
  'NODE_EXTRA_CA_CERTS', 'NODE_NO_WARNINGS', 'DISABLE_AUTOUPDATER',
@@ -1138,9 +1365,23 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
1138
1365
  'ATRIS_VALIDATOR_MODE', 'ATRIS_VALIDATOR_COUNT', 'ATRIS_VALIDATOR_PROMPT',
1139
1366
  'ATRIS_PUSH_RESULT', 'ATRIS_PUSH_URL', 'ATRIS_PUSH_CONFIG', 'ATRIS_BOUNDARY_DUMP',
1140
1367
  'ATRIS_REAL_GIT', 'ATRIS_TASKS_DB',
1141
- ]) {
1368
+ ];
1369
+ for (const key of inheritKeys) {
1142
1370
  if (process.env[key] !== undefined) environment[key] = String(process.env[key]);
1143
1371
  }
1372
+ if (options.secretGrant) {
1373
+ const grantOptions = {};
1374
+ if (options.secretGrantUpstreamPort !== undefined) grantOptions.upstreamPort = options.secretGrantUpstreamPort;
1375
+ if (options.secretGrantUpstreamAddress !== undefined) grantOptions.upstreamAddress = options.secretGrantUpstreamAddress;
1376
+ if (options.secretGrantRejectUnauthorized !== undefined) {
1377
+ grantOptions.rejectUnauthorized = options.secretGrantRejectUnauthorized;
1378
+ }
1379
+ applySecretGrantEnvironment(environment, options.secretGrant, grantOptions);
1380
+ for (const key of SECRET_GATEWAY_PROXY_ENV_KEYS) {
1381
+ if (key.toUpperCase() === 'NO_PROXY') environment[key] = '127.0.0.1,localhost';
1382
+ else environment[key] = '';
1383
+ }
1384
+ }
1144
1385
  return environment;
1145
1386
  }
1146
1387
 
@@ -1968,8 +2209,7 @@ function dispatchReceiptResult(flight, { ids, reviewOnly, enforceRemoteBoundary
1968
2209
  return result;
1969
2210
  }
1970
2211
 
1971
- function writeDispatchReceipt(flight, receiptPath, resultOptions) {
1972
- flight.result = dispatchReceiptResult(flight, resultOptions);
2212
+ function atomicWriteFlightReceipt(flight, receiptPath) {
1973
2213
  flight.receipt = receiptPath;
1974
2214
  fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
1975
2215
  const tempPath = `${receiptPath}.tmp-${process.pid}`;
@@ -1977,6 +2217,21 @@ function writeDispatchReceipt(flight, receiptPath, resultOptions) {
1977
2217
  fs.renameSync(tempPath, receiptPath);
1978
2218
  }
1979
2219
 
2220
+ function startLiveFlight(root, receiptPath, flight) {
2221
+ const liveLogPath = createEngineLiveLog(receiptPath);
2222
+ flight.status = 'running';
2223
+ flight.pid = process.pid;
2224
+ flight.started_at = flight.started_at || flight.at || new Date().toISOString();
2225
+ flight.live_log = path.relative(root, liveLogPath) || liveLogPath;
2226
+ atomicWriteFlightReceipt(flight, receiptPath);
2227
+ return liveLogPath;
2228
+ }
2229
+
2230
+ function writeDispatchReceipt(flight, receiptPath, resultOptions) {
2231
+ flight.result = dispatchReceiptResult(flight, resultOptions);
2232
+ atomicWriteFlightReceipt(flight, receiptPath);
2233
+ }
2234
+
1980
2235
  function stampDispatchResultVerification(flight, taskId, engine, startedAtMs, verifierResult) {
1981
2236
  if (!verifierResult || typeof verifierResult.passed !== 'boolean') return;
1982
2237
  const row = flight.results.find((result) => result.task === taskId);
@@ -2110,19 +2365,76 @@ function defaultTrustedVerifyRunner(command, cwd) {
2110
2365
  };
2111
2366
  }
2112
2367
 
2113
- function defaultSelfLandCheck({ worktreePath, targetRef = DISPATCH_SELF_LAND_TARGET, git = null } = {}) {
2368
+ function defaultDispatchStartCommit({ worktreePath, git = null } = {}) {
2369
+ const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
2370
+ const head = run(['rev-parse', '--verify', 'HEAD^{commit}']);
2371
+ if (!head || head.status !== 0) return '';
2372
+ return String(head.stdout || '').trim();
2373
+ }
2374
+
2375
+ function defaultSelfLandCheck({ worktreePath, targetRef = DISPATCH_SELF_LAND_TARGET, startCommit = '', entry = null, git = null } = {}) {
2114
2376
  const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
2377
+ const recordedStart = String(startCommit || (entry && entry.startCommit) || '').trim();
2378
+ if (!recordedStart) {
2379
+ return { ok: false, stage: 'self_land_check', reason: 'unknown_start_commit', target: targetRef, detail: 'dispatch start commit was not recorded' };
2380
+ }
2381
+ const headResult = run(['rev-parse', '--verify', 'HEAD^{commit}']);
2382
+ if (!headResult || headResult.status !== 0) {
2383
+ return {
2384
+ ok: false,
2385
+ stage: 'self_land_check',
2386
+ reason: 'unknown_head',
2387
+ target: targetRef,
2388
+ start_commit: recordedStart,
2389
+ detail: String(headResult && (headResult.stderr || headResult.stdout) || '').trim(),
2390
+ };
2391
+ }
2392
+ const head = String(headResult.stdout || '').trim();
2393
+ if (head === recordedStart) {
2394
+ return {
2395
+ ok: false,
2396
+ stage: 'no_work_landed',
2397
+ reason: 'no_work_landed',
2398
+ target: targetRef,
2399
+ start_commit: recordedStart,
2400
+ head,
2401
+ detail: 'no work landed because HEAD still matches the dispatch start commit',
2402
+ };
2403
+ }
2404
+ const diff = run(['diff', '--quiet', recordedStart, head, '--']);
2405
+ if (diff.status === 0) {
2406
+ return {
2407
+ ok: false,
2408
+ stage: 'no_work_landed',
2409
+ reason: 'no_work_landed',
2410
+ target: targetRef,
2411
+ start_commit: recordedStart,
2412
+ head,
2413
+ detail: 'no work landed because the commit changed without changing the tree',
2414
+ };
2415
+ }
2416
+ if (diff.status !== 1) {
2417
+ return {
2418
+ ok: false,
2419
+ stage: 'self_land_check',
2420
+ reason: 'diff_check_failed',
2421
+ target: targetRef,
2422
+ start_commit: recordedStart,
2423
+ head,
2424
+ detail: String(diff.stderr || diff.stdout || '').trim(),
2425
+ };
2426
+ }
2115
2427
  const branch = String(targetRef || '').startsWith('origin/') ? String(targetRef).slice('origin/'.length) : '';
2116
2428
  const fetch = run(branch ? ['fetch', 'origin', `${branch}:refs/remotes/origin/${branch}`] : ['fetch', 'origin']);
2117
2429
  if (fetch.status !== 0) {
2118
- return { ok: false, stage: 'self_land_check', target: targetRef, detail: String(fetch.stderr || fetch.stdout || '').trim() };
2430
+ return { ok: false, stage: 'self_land_check', target: targetRef, start_commit: recordedStart, head, detail: String(fetch.stderr || fetch.stdout || '').trim() };
2119
2431
  }
2120
2432
  const ancestor = run(['merge-base', '--is-ancestor', 'HEAD', targetRef]);
2121
- if (ancestor.status === 0) return { ok: true, stage: 'self_landed', target: targetRef };
2433
+ if (ancestor.status === 0) return { ok: true, stage: 'self_landed', target: targetRef, start_commit: recordedStart, head };
2122
2434
  if (ancestor.status === 1) {
2123
- return { ok: false, stage: 'self_land_missing', target: targetRef, detail: `HEAD is not an ancestor of ${targetRef}` };
2435
+ return { ok: false, stage: 'self_land_missing', target: targetRef, start_commit: recordedStart, head, detail: `HEAD is not an ancestor of ${targetRef}` };
2124
2436
  }
2125
- return { ok: false, stage: 'self_land_check', target: targetRef, detail: String(ancestor.stderr || ancestor.stdout || '').trim() };
2437
+ return { ok: false, stage: 'self_land_check', target: targetRef, start_commit: recordedStart, head, detail: String(ancestor.stderr || ancestor.stdout || '').trim() };
2126
2438
  }
2127
2439
 
2128
2440
  // One flight. Staff -> dispatch in parallel -> land serially -> receipt.
@@ -2140,6 +2452,7 @@ async function runFleetFlight({
2140
2452
  rebase = null,
2141
2453
  checkoutBase = 'origin/master',
2142
2454
  guardCliLink = guardGlobalCliLink,
2455
+ scoutAsk = null,
2143
2456
  } = {}) {
2144
2457
  const cli = ownCli || defaultOwnCli(root);
2145
2458
  // Staff first, rank second: every task staffFlight returns already cleared
@@ -2188,6 +2501,8 @@ async function runFleetFlight({
2188
2501
  return flight;
2189
2502
  }
2190
2503
 
2504
+ const liveLogPath = startLiveFlight(root, receiptPath, flight);
2505
+
2191
2506
  // Claim + cut a worktree per assignment, then dispatch all in parallel.
2192
2507
  const dispatch = dispatcher || ((entry) => new Promise((resolve) => {
2193
2508
  resolve(dispatchToEngine({
@@ -2198,6 +2513,7 @@ async function runFleetFlight({
2198
2513
  prompt: entry.prompt,
2199
2514
  briefId: entry.brief_id,
2200
2515
  skipBriefCapture: true,
2516
+ liveLogPath,
2201
2517
  }));
2202
2518
  }));
2203
2519
  const restaffState = { used: false };
@@ -2211,7 +2527,13 @@ async function runFleetFlight({
2211
2527
  const startBaseArgs = checkoutBase ? ['--base', checkoutBase] : [];
2212
2528
  const prepared = [];
2213
2529
  for (const entry of staffed) {
2214
- cli(['task', 'claim', String(entry.task.display_id), '--as', `fleet-${entry.engine}`]);
2530
+ const claimed = cli(['task', 'claim', String(entry.task.display_id), '--as', `fleet-${entry.engine}`]);
2531
+ if (!claimed || claimed.status !== 0) {
2532
+ const detail = String(claimed && (claimed.stderr || claimed.stdout) || 'claim failed').trim().slice(0, 300);
2533
+ flight.paused.push({ task: entry.task.display_id, engine: entry.engine, stage: 'claim', detail });
2534
+ log(` ${entry.engine.padEnd(8)} ⏸ paused ${entry.task.display_id} at claim${detail ? ` — ${detail}` : ''}`);
2535
+ continue;
2536
+ }
2215
2537
  const started = cli(['worktree', 'start', '--agent', entry.engine, '--task', `fleet-${String(entry.task.display_id).toLowerCase()}`, ...startBaseArgs]);
2216
2538
  const wt = (started.stdout.match(/next: cd (.+)/) || [])[1];
2217
2539
  if (!wt) {
@@ -2230,6 +2552,7 @@ async function runFleetFlight({
2230
2552
  dispatch,
2231
2553
  installedEngines: roster,
2232
2554
  restaffState,
2555
+ scoutAsk: scoutAsk === null && dispatcher ? false : (scoutAsk || undefined),
2233
2556
  }).then((result) => ({ entry, result }))
2234
2557
  ));
2235
2558
  flight.results = results.map(({ entry, result }) => {
@@ -2257,8 +2580,15 @@ async function runFleetFlight({
2257
2580
  const activeEngine = result.engine || entry.engine;
2258
2581
  const landingEntry = { ...entry, engine: activeEngine };
2259
2582
  if (result.restaffed) log(` restaffed ${entry.task.display_id}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
2260
- if (result.exitCode !== 0) {
2261
- const paused = { task: entry.task.display_id, engine: activeEngine, stage: 'build', detail: (result.stderr || '').slice(0, 200) };
2583
+ const buildFailure = detectDeadEngineDispatch(result);
2584
+ if (buildFailure) {
2585
+ const paused = {
2586
+ task: entry.task.display_id,
2587
+ engine: activeEngine,
2588
+ stage: 'build',
2589
+ reason: buildFailure.reason,
2590
+ detail: String(result.stderr || buildFailure.reason).slice(0, 200),
2591
+ };
2262
2592
  if (result.restaffed) paused.restaffed = result.restaffed;
2263
2593
  if (result.deadEngine) paused.deadEngine = result.deadEngine;
2264
2594
  flight.paused.push(paused);
@@ -2301,9 +2631,9 @@ async function runFleetFlight({
2301
2631
  : ` alert: global atris link was ${cliLink.reason}; repoint attempted at ${cliLink.restoredTo} but the cli still fails, so every cron/loop/mission is down`);
2302
2632
  }
2303
2633
 
2304
- fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
2305
- flight.receipt = receiptPath;
2306
- fs.writeFileSync(flight.receipt, `${JSON.stringify(flight, null, 2)}\n`);
2634
+ flight.status = flight.paused.length ? 'failed' : 'completed';
2635
+ flight.finished_at = new Date().toISOString();
2636
+ atomicWriteFlightReceipt(flight, receiptPath);
2307
2637
  log('');
2308
2638
  log(` flight over: ${flight.landed.length} landed, ${flight.paused.length} paused · receipt: ${path.relative(root, flight.receipt)}`);
2309
2639
  log('');
@@ -2335,6 +2665,7 @@ async function runDispatchFlight({
2335
2665
  checkoutBase = 'origin/master',
2336
2666
  installedEngines = null,
2337
2667
  selfLandCheck = null,
2668
+ startCommitReader = null,
2338
2669
  yolo = false,
2339
2670
  reviewOnly = false,
2340
2671
  verifierCommand = '',
@@ -2344,6 +2675,7 @@ async function runDispatchFlight({
2344
2675
  validatorEngines = null,
2345
2676
  validatorDispatcher = null,
2346
2677
  validatorStateInspector = null,
2678
+ scoutAsk = null,
2347
2679
  } = {}) {
2348
2680
  if (!engine) throw new Error('runDispatchFlight: engine is required');
2349
2681
  if (!FLEET_CAPABLE.includes(engine)) {
@@ -2368,6 +2700,7 @@ async function runDispatchFlight({
2368
2700
  const cli = ownCli || defaultOwnCli(root);
2369
2701
  const verify = verifier || (trustedVerifier ? defaultTrustedVerifyRunner : defaultVerifyRunner);
2370
2702
  const inspectChange = changeInspector || inspectReviewChange;
2703
+ const readStartCommit = startCommitReader || defaultDispatchStartCommit;
2371
2704
  const enforceRemoteBoundary = reviewOnly && !dispatcher;
2372
2705
  const explicitActor = String(actor || '').trim();
2373
2706
  const taskActor = explicitActor || `fleet-${engine}`;
@@ -2388,6 +2721,7 @@ async function runDispatchFlight({
2388
2721
  flight.actor = taskActor;
2389
2722
  if (receiptContext && typeof receiptContext === 'object') flight.context = receiptContext;
2390
2723
  const requiresIndependentValidator = reviewOnly && flight.context && flight.context.source === 'one_lap';
2724
+ const liveLogPath = startLiveFlight(root, receiptPath, flight);
2391
2725
 
2392
2726
  log('');
2393
2727
  log(` dispatch — ${ids.length} task${ids.length === 1 ? '' : 's'} -> ${engine}`);
@@ -2436,6 +2770,7 @@ async function runDispatchFlight({
2436
2770
  continue;
2437
2771
  }
2438
2772
  const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
2773
+ const startCommit = yolo ? readStartCommit({ worktreePath }) : '';
2439
2774
  const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
2440
2775
  const safetyPrompt = reviewOnly
2441
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.`
@@ -2451,6 +2786,7 @@ async function runDispatchFlight({
2451
2786
  engine,
2452
2787
  remoteBoundary,
2453
2788
  remoteMasterBefore: remoteBoundary ? remoteBoundary.protectedMaster : '',
2789
+ startCommit,
2454
2790
  ...(trustedPrompt ? { prompt: trustedPrompt } : {}),
2455
2791
  });
2456
2792
  log(` building ${taskId} in ${path.basename(worktreePath)}`);
@@ -2472,6 +2808,7 @@ async function runDispatchFlight({
2472
2808
  yolo,
2473
2809
  briefId: entry.brief_id,
2474
2810
  skipBriefCapture: true,
2811
+ liveLogPath,
2475
2812
  }));
2476
2813
  }));
2477
2814
  const restaffState = { used: false };
@@ -2485,6 +2822,7 @@ async function runDispatchFlight({
2485
2822
  dispatch,
2486
2823
  installedEngines,
2487
2824
  restaffState,
2825
+ scoutAsk: scoutAsk === null && dispatcher ? false : (scoutAsk || undefined),
2488
2826
  }).then((result) => {
2489
2827
  const completedAtMs = Date.now();
2490
2828
  return {
@@ -2492,7 +2830,7 @@ async function runDispatchFlight({
2492
2830
  result,
2493
2831
  startedAtMs,
2494
2832
  completedAtMs,
2495
- candidate: enforceRemoteBoundary && result.exitCode === 0
2833
+ candidate: enforceRemoteBoundary && !detectDeadEngineDispatch(result)
2496
2834
  ? reviewCandidateSnapshot(entry.remoteBoundary)
2497
2835
  : null,
2498
2836
  };
@@ -2508,6 +2846,7 @@ async function runDispatchFlight({
2508
2846
  at: new Date(completedAtMs).toISOString(),
2509
2847
  exitCode: result.exitCode,
2510
2848
  };
2849
+ if (entry.startCommit) row.start_commit = entry.startCommit;
2511
2850
  if (result.brief_id) row.brief_id = result.brief_id;
2512
2851
  if (result.restaffed) row.restaffed = result.restaffed;
2513
2852
  if (result.deadEngine) row.deadEngine = result.deadEngine;
@@ -2602,12 +2941,14 @@ async function runDispatchFlight({
2602
2941
  const readyActor = explicitActor || `fleet-${activeEngine}`;
2603
2942
  const landingEntry = { ...entry, engine: activeEngine, candidate };
2604
2943
  if (result.restaffed) log(` restaffed ${entry.taskId}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
2605
- if (result.exitCode !== 0) {
2944
+ const buildFailure = detectDeadEngineDispatch(result);
2945
+ if (buildFailure) {
2606
2946
  const paused = {
2607
2947
  task: entry.taskId,
2608
2948
  engine: activeEngine,
2609
2949
  stage: 'build',
2610
- detail: String(result.stderr || '').slice(0, 300),
2950
+ reason: buildFailure.reason,
2951
+ detail: String(result.stderr || buildFailure.reason).slice(0, 300),
2611
2952
  worktree: entry.worktreePath,
2612
2953
  };
2613
2954
  if (result.restaffed) paused.restaffed = result.restaffed;
@@ -2649,7 +2990,13 @@ async function runDispatchFlight({
2649
2990
  }
2650
2991
  if (yolo) {
2651
2992
  log(` checking self-land ${entry.taskId}...`);
2652
- const selfLanded = checkSelfLand({ entry, result, worktreePath: entry.worktreePath, targetRef: DISPATCH_SELF_LAND_TARGET });
2993
+ const selfLanded = checkSelfLand({
2994
+ entry,
2995
+ result,
2996
+ worktreePath: entry.worktreePath,
2997
+ targetRef: DISPATCH_SELF_LAND_TARGET,
2998
+ startCommit: entry.startCommit,
2999
+ });
2653
3000
  if (selfLanded.ok) {
2654
3001
  const target = selfLanded.target || DISPATCH_SELF_LAND_TARGET;
2655
3002
  flight.landed.push({
@@ -2658,10 +3005,10 @@ async function runDispatchFlight({
2658
3005
  landing: 'self',
2659
3006
  target,
2660
3007
  verifier_result: {
2661
- command: `git merge-base --is-ancestor HEAD ${target}`,
3008
+ command: `HEAD and its tree differ from dispatch start commit; git merge-base --is-ancestor HEAD ${target}`,
2662
3009
  passed: true,
2663
3010
  status: 0,
2664
- output: `HEAD is an ancestor of ${target}`,
3011
+ output: `HEAD ${selfLanded.head || ''} differs from ${selfLanded.start_commit || entry.startCommit} and is an ancestor of ${target}`,
2665
3012
  },
2666
3013
  ...(result.brief_id ? { brief_id: result.brief_id } : {}),
2667
3014
  });
@@ -2676,8 +3023,12 @@ async function runDispatchFlight({
2676
3023
  target: selfLanded.target || DISPATCH_SELF_LAND_TARGET,
2677
3024
  detail: selfLanded.detail || '',
2678
3025
  });
2679
- stampDispatchBrief(root, result.brief_id, 'partial', `self-land check paused ${entry.taskId} at ${stage}`);
2680
- log(` ⏸ paused ${entry.taskId} at ${stage}`);
3026
+ stampDispatchBrief(root, result.brief_id, 'partial', stage === 'no_work_landed'
3027
+ ? `no work landed for ${entry.taskId}`
3028
+ : `self-land check paused ${entry.taskId} at ${stage}`);
3029
+ log(stage === 'no_work_landed'
3030
+ ? ` ⏸ no work landed for ${entry.taskId}`
3031
+ : ` ⏸ paused ${entry.taskId} at ${stage}`);
2681
3032
  }
2682
3033
  continue;
2683
3034
  }
@@ -2973,6 +3324,8 @@ async function runDispatchFlight({
2973
3324
  }
2974
3325
  }
2975
3326
 
3327
+ flight.status = flight.paused.length ? 'failed' : 'completed';
3328
+ flight.finished_at = new Date().toISOString();
2976
3329
  writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
2977
3330
  log('');
2978
3331
  const completedLabel = reviewOnly ? `${flight.ready.length} proof ready` : `${flight.landed.length} landed`;