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.
- package/atris/skills/design/SKILL.md +7 -1
- package/atris/skills/engines/SKILL.md +44 -13
- package/atris/team/customer-lead/MEMBER.md +45 -0
- package/atris/team/customer-lead/SOUL.md +33 -0
- package/atris/team/customer-lead/START_HERE.md +7 -0
- package/atris/team/customer-lead/skills/customer-commitments/SKILL.md +45 -0
- package/atris/team/improver/MEMBER.md +33 -0
- package/bin/atris.js +37 -4
- package/commands/autoland.js +15 -1
- package/commands/caretaker.js +303 -0
- package/commands/clean.js +76 -0
- package/commands/engine-watch.js +212 -0
- package/commands/engine.js +99 -11
- package/commands/founder.js +304 -0
- package/commands/human-missions.js +844 -0
- package/commands/init.js +16 -7
- package/commands/lesson.js +178 -4
- package/commands/mission.js +124 -69
- package/commands/slop.js +34 -3
- package/commands/task.js +51 -4
- package/commands/team.js +329 -13
- package/commands/verify.js +99 -6
- package/commands/worktree.js +119 -4
- package/lib/auto-accept-certified.js +302 -0
- package/lib/cloud-mission.js +59 -2
- package/lib/conductor-artifacts.js +1 -1
- package/lib/dispatch-scout.js +383 -0
- package/lib/engine-ask.js +645 -0
- package/lib/engine-job-lifecycle.js +65 -0
- package/lib/engine-receipt-sweep.js +98 -0
- package/lib/engine-registry.js +2 -2
- package/lib/engine-validate.js +374 -0
- package/lib/fleet.js +459 -106
- package/lib/known-commands.js +2 -2
- package/lib/lesson-ledger.js +84 -0
- package/lib/member-alive.js +2 -2
- package/lib/policy-lessons.js +70 -0
- package/lib/receipt-evidence.js +56 -1
- package/lib/runner-command.js +1 -1
- package/lib/secret-gateway.js +588 -0
- package/lib/team-presence.js +13 -1
- package/lib/voice-gate.js +6 -0
- package/lib/wish-audit.js +5 -205
- package/lib/wish-delegate.js +5 -2
- 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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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],
|
|
445
|
+
result = spawnSync(process.execPath, [supervisor, stateFile, leaseFile, statusFile, executable, ...args], spawnOptions);
|
|
306
446
|
} finally {
|
|
307
|
-
terminateSameSandboxProfile(executable, args,
|
|
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
|
|
342
|
-
if (Object.prototype.hasOwnProperty.call(result, 'exitCode'))
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
404
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
715
|
+
const runtimePath = path.dirname(promptFile);
|
|
716
|
+
fs.mkdirSync(runtimePath, { recursive: true });
|
|
551
717
|
fs.writeFileSync(promptFile, prompt);
|
|
552
|
-
|
|
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:
|
|
765
|
+
timeout: dispatchTimeoutMs,
|
|
577
766
|
};
|
|
578
|
-
|
|
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 (
|
|
583
|
-
if (cleanupControlDir) fs.rmSync(cleanupControlDir, { recursive: true, force: true });
|
|
780
|
+
if (!deferredCleanup) cleanup();
|
|
584
781
|
}
|
|
585
782
|
});
|
|
586
|
-
const result = exec(command);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2261
|
-
|
|
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
|
-
|
|
2305
|
-
flight.
|
|
2306
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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',
|
|
2680
|
-
|
|
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`;
|