atris 3.60.0 → 3.62.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/README.md +37 -5
- package/atris/skills/engines/SKILL.md +9 -9
- package/bin/atris.js +1 -0
- package/commands/autoland.js +36 -5
- package/commands/autopilot.js +105 -6
- package/commands/engine.js +96 -21
- package/commands/host.js +56 -4
- package/commands/land.js +129 -8
- package/commands/member.js +33 -8
- package/commands/mission.js +112 -3
- package/commands/one-lap.js +14 -10
- package/commands/stream.js +56 -22
- package/commands/team.js +82 -10
- package/commands/worktree.js +39 -0
- package/commands/youtube.js +36 -26
- package/lib/engine-ask.js +112 -9
- package/lib/engine-registry.js +283 -39
- package/lib/fleet.js +444 -26
- package/lib/functional-owner.js +15 -6
- package/lib/host.js +311 -175
- package/lib/member-engine.js +11 -1
- package/lib/member-park.js +138 -0
- package/lib/member-scaffold.js +37 -0
- package/lib/roster-markdown.js +1 -1
- package/lib/roster-prep.js +289 -0
- package/lib/roster-runs.js +389 -0
- package/lib/roster-suggest.js +145 -0
- package/lib/router-brain.js +111 -3
- package/lib/self-drive.js +26 -2
- package/lib/state-detection.js +3 -3
- package/lib/task-db.js +38 -3
- package/lib/team-lineup.js +7 -2
- package/lib/todo.js +1 -1
- package/lib/wish-audit.js +1 -1
- package/package.json +1 -1
- package/scripts/det/cpu-hotspots.js +216 -0
- package/scripts/det/ytnotes +105 -19
- package/scripts/det/ytquote-repair.js +18 -4
- package/templates/members/host/MEMBER.md +5 -3
- package/templates/members/host/MISSION.md +1 -1
- package/templates/members/host/skills/host/SKILL.md +61 -12
package/lib/fleet.js
CHANGED
|
@@ -23,7 +23,9 @@ const {
|
|
|
23
23
|
worktreeBaseRef,
|
|
24
24
|
} = require('./brief-ledger');
|
|
25
25
|
const { RUNNER_PROFILE_DEFS, buildRunnerCommand } = require('./runner-command');
|
|
26
|
-
const { engineFailureHealthStatus,
|
|
26
|
+
const { setEngineHealth, recordEngineRunHealth, engineStallReason, engineFailureHealthStatus, resolveJobTeam, rosterDecided, rosterPinFor } = require('./engine-registry');
|
|
27
|
+
const { appendRosterAttempts, parseRunUsage } = require('./roster-runs');
|
|
28
|
+
const { engineRunsView } = require('./roster-models');
|
|
27
29
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
28
30
|
const { rankEnginesDetailed } = require('./router-brain');
|
|
29
31
|
const {
|
|
@@ -37,6 +39,7 @@ const { sleepSync } = require('./sleep-sync');
|
|
|
37
39
|
const { ownerIdentity } = require('../utils/owner-identity');
|
|
38
40
|
const { matchTaste } = require('./taste-lessons');
|
|
39
41
|
const { buildVerifiedScoutPack, appendVerifiedScoutPack } = require('./dispatch-scout');
|
|
42
|
+
const { runPrepPass, withPrepBrief, prepRecordFields } = require('./roster-prep');
|
|
40
43
|
const { mapNotesPath } = require('./map-refs');
|
|
41
44
|
const {
|
|
42
45
|
applySecretGrantEnvironment,
|
|
@@ -220,7 +223,17 @@ function validatorPin(validatorModels, engine) {
|
|
|
220
223
|
const pin = validatorModels && validatorModels[engine];
|
|
221
224
|
if (!pin) return { model: '', effort: '', maxSeconds: 0 };
|
|
222
225
|
if (typeof pin === 'string') return { model: pin, effort: '', maxSeconds: 0 };
|
|
223
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
model: String(pin.model || ''),
|
|
228
|
+
effort: String(pin.effort || ''),
|
|
229
|
+
maxSeconds: capSeconds(pin.max_seconds),
|
|
230
|
+
...(pin.prep ? { prep: String(pin.prep) } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// The engine flags a pin sets, never its prep job.
|
|
235
|
+
function pinEngineFlags(pin) {
|
|
236
|
+
return { model: pin.model, effort: pin.effort, maxSeconds: pin.maxSeconds };
|
|
224
237
|
}
|
|
225
238
|
|
|
226
239
|
function wrapCodexWithWatchdog(cmd, watchdogPath = CODEX_WATCHDOG_SOURCE, receiptPath = '', maxSeconds = 0) {
|
|
@@ -650,9 +663,20 @@ function detectDeadEngineDispatch(result) {
|
|
|
650
663
|
const signal = dispatchResultSignal(result);
|
|
651
664
|
if (signal) return { reason: 'signalled', signal, exitCode };
|
|
652
665
|
if (terminal.reason === 'ok') return null;
|
|
653
|
-
|
|
666
|
+
// A credit wall is read from engine-level signals only. The report and
|
|
667
|
+
// stdout are the task's own work: a failing "rate limit" test says nothing
|
|
668
|
+
// about the engine's credits.
|
|
669
|
+
const rateLimitInfo = result && result.rate_limit_info;
|
|
670
|
+
const rateLimitStatus = String(
|
|
671
|
+
rateLimitInfo && typeof rateLimitInfo === 'object' ? rateLimitInfo.status || '' : rateLimitInfo || '',
|
|
672
|
+
).trim().toLowerCase();
|
|
673
|
+
const engineText = [
|
|
674
|
+
result && result.stderr,
|
|
675
|
+
result && result.error && (result.error.message || result.error.code || result.error),
|
|
676
|
+
rateLimitStatus && rateLimitStatus !== 'allowed' ? `rate limit ${rateLimitStatus}` : '',
|
|
677
|
+
].filter(Boolean).map(String).join('\n').toLowerCase();
|
|
654
678
|
const pattern = Number.isInteger(exitCode) && exitCode !== 0
|
|
655
|
-
? DEAD_ENGINE_OUTPUT_PATTERNS.find((p) =>
|
|
679
|
+
? DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => engineText.includes(p))
|
|
656
680
|
: null;
|
|
657
681
|
if (pattern) return { reason: 'usage_limit', pattern };
|
|
658
682
|
if (terminal.reason === 'no_output' || terminal.reason === 'unknown' || terminal.reason === 'timeout' || terminal.reason === 'cancelled') {
|
|
@@ -680,16 +704,15 @@ function plainDispatchFailureCause(result, failure = {}) {
|
|
|
680
704
|
return (reason || 'the engine failed').replace(/_/g, ' ');
|
|
681
705
|
}
|
|
682
706
|
|
|
707
|
+
// A stalled run (time cap, watchdog, dropped connection) benches its engine
|
|
708
|
+
// for the cooldown; other failures keep today's health write.
|
|
683
709
|
function recordDispatchEngineHealth(result, failure, root) {
|
|
684
710
|
if (!result || !result.engine) return null;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
})
|
|
691
|
-
: 'ready';
|
|
692
|
-
return status ? setEngineHealth(result.engine, status, root) : null;
|
|
711
|
+
if (!failure) return setEngineHealth(result.engine, 'ready', root);
|
|
712
|
+
return recordEngineRunHealth(result.engine, {
|
|
713
|
+
...result,
|
|
714
|
+
reason: [result.reason, failure.reason].filter(Boolean).join('\n'),
|
|
715
|
+
}, root);
|
|
693
716
|
}
|
|
694
717
|
|
|
695
718
|
function normalizeInstalledEngines(engines) {
|
|
@@ -754,6 +777,97 @@ function failedDispatchLeg(result, engineName) {
|
|
|
754
777
|
return leg;
|
|
755
778
|
}
|
|
756
779
|
|
|
780
|
+
// How one attempt ended, before landing decides the last one: a credit wall,
|
|
781
|
+
// a stall with its plain text, a real task failure, or provisionally landed.
|
|
782
|
+
// Same engine-level rule as the health write: the task's own words never
|
|
783
|
+
// decide the engine's outcome.
|
|
784
|
+
function rosterAttemptOutcome(result) {
|
|
785
|
+
const dead = detectDeadEngineDispatch(result);
|
|
786
|
+
if (!dead) return { outcome: 'landed' };
|
|
787
|
+
const health = engineFailureHealthStatus({
|
|
788
|
+
...result,
|
|
789
|
+
status: 'errored',
|
|
790
|
+
reason: [result && result.reason, dead.reason].filter(Boolean).join('\n'),
|
|
791
|
+
});
|
|
792
|
+
if (health === 'credit_out') return { outcome: 'credit out', detail: dead.pattern ? `said "${dead.pattern}"` : plainDispatchFailureCause(result, dead) };
|
|
793
|
+
const stall = engineStallReason(result);
|
|
794
|
+
if (stall) return { outcome: 'stalled', detail: stall.text };
|
|
795
|
+
return { outcome: 'failed', detail: plainDispatchFailureCause(result, dead) };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// One attempt's line: the worker as it really ran (its pin, else the model
|
|
799
|
+
// the engine runs on its own), the seconds, and tokens only when printed.
|
|
800
|
+
function rosterAttemptRecord(engineName, pin, startedMs, endedMs, result, prep = null) {
|
|
801
|
+
const model = (pin && pin.model) || engineRunsView(engineName).model || '';
|
|
802
|
+
return {
|
|
803
|
+
...prepRecordFields(prep),
|
|
804
|
+
at: new Date(startedMs).toISOString(),
|
|
805
|
+
engine: engineName,
|
|
806
|
+
...(model ? { model } : {}),
|
|
807
|
+
...(pin && pin.effort ? { effort: pin.effort } : {}),
|
|
808
|
+
...(pin && pin.maxSeconds ? { max_seconds: pin.maxSeconds } : {}),
|
|
809
|
+
seconds: Math.max(0, (endedMs - startedMs) / 1000),
|
|
810
|
+
...parseRunUsage({
|
|
811
|
+
usage: result && result.usage,
|
|
812
|
+
cost_usd: result && result.cost_usd,
|
|
813
|
+
stdout: result && (result.report || result.stdout),
|
|
814
|
+
stderr: result && result.stderr,
|
|
815
|
+
}),
|
|
816
|
+
...rosterAttemptOutcome(result),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// After landing: the last attempt landed only when its task is in the landed
|
|
821
|
+
// or proof-ready rows; otherwise it failed at the stage that paused it. Every
|
|
822
|
+
// earlier attempt keeps its own outcome and names who took over.
|
|
823
|
+
function recordFlightRosterRuns(root, flight, results, { job = 'build', member = '', source = '' } = {}) {
|
|
824
|
+
for (const { entry, result } of results || []) {
|
|
825
|
+
const attempts = (result && Array.isArray(result.attempts) ? result.attempts : []).map((attempt) => ({ ...attempt }));
|
|
826
|
+
if (!attempts.length) continue;
|
|
827
|
+
const taskId = String(entry.taskId || dispatchTaskId(entry.task));
|
|
828
|
+
const sameTask = (row) => row && String(row.task) === taskId;
|
|
829
|
+
const last = attempts[attempts.length - 1];
|
|
830
|
+
if (last.outcome === 'landed') {
|
|
831
|
+
const landed = (flight.landed || []).some(sameTask) || (flight.ready || []).some(sameTask);
|
|
832
|
+
const paused = (flight.paused || []).find(sameTask);
|
|
833
|
+
if (!landed) {
|
|
834
|
+
last.outcome = 'failed';
|
|
835
|
+
last.detail = paused
|
|
836
|
+
? `paused at ${String(paused.stage || 'landing').replace(/_/g, ' ')}${paused.detail ? `: ${String(paused.detail).split('\n')[0]}` : ''}`
|
|
837
|
+
: 'did not land';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
appendRosterAttempts(root, attempts, {
|
|
841
|
+
job: entry.roster_job || job,
|
|
842
|
+
...(member ? { member } : {}),
|
|
843
|
+
task: taskId,
|
|
844
|
+
...(source ? { source } : {}),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// A dispatch no flight watches (a mission blocker's build): the same health
|
|
850
|
+
// write and run line the flight paths make, so a stall benches the engine
|
|
851
|
+
// and the record shows how it did. Never throws.
|
|
852
|
+
function recordDispatchOutcome(root, { engine, pin = {}, startedMs, endedMs, result, job = 'build', task = '', source = '', member = '' } = {}) {
|
|
853
|
+
try {
|
|
854
|
+
const run = normalizeDispatchResult(result, engine);
|
|
855
|
+
if (!run.max_seconds && pin && pin.maxSeconds) run.max_seconds = pin.maxSeconds;
|
|
856
|
+
const dead = detectDeadEngineDispatch(run);
|
|
857
|
+
recordDispatchEngineHealth(run, dead, root);
|
|
858
|
+
const attempt = rosterAttemptRecord(engine, pin, startedMs, endedMs, run);
|
|
859
|
+
appendRosterAttempts(root, [attempt], {
|
|
860
|
+
job,
|
|
861
|
+
...(task ? { task: String(task) } : {}),
|
|
862
|
+
...(source ? { source } : {}),
|
|
863
|
+
...(member ? { member } : {}),
|
|
864
|
+
});
|
|
865
|
+
return { outcome: attempt.outcome, ...(attempt.detail ? { detail: attempt.detail } : {}) };
|
|
866
|
+
} catch {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
757
871
|
async function dispatchEntryWithRestaff({
|
|
758
872
|
entry,
|
|
759
873
|
engine,
|
|
@@ -762,8 +876,55 @@ async function dispatchEntryWithRestaff({
|
|
|
762
876
|
installedEngines = null,
|
|
763
877
|
restaffState,
|
|
764
878
|
scoutAsk,
|
|
879
|
+
clock = Date.now,
|
|
880
|
+
defaultPin = null,
|
|
881
|
+
job = 'build',
|
|
882
|
+
prepAsk = undefined,
|
|
883
|
+
}) {
|
|
884
|
+
const attempts = [];
|
|
885
|
+
const result = await dispatchEntryAttempts({ entry, engine, root, dispatch, installedEngines, restaffState, scoutAsk, clock, defaultPin, attempts, job, prepAsk });
|
|
886
|
+
return { ...result, attempts };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// One prep pass per prep job per task: a backup whose line asks for the same
|
|
890
|
+
// prep job reuses the brief the lead's prep already wrote.
|
|
891
|
+
function prepPassCache({ entry, root, clock, prepAsk, job }) {
|
|
892
|
+
const passes = new Map();
|
|
893
|
+
return (prepJob, prompt) => {
|
|
894
|
+
if (!prepJob) return Promise.resolve(null);
|
|
895
|
+
if (!passes.has(prepJob)) {
|
|
896
|
+
passes.set(prepJob, runPrepPass({
|
|
897
|
+
prepJob,
|
|
898
|
+
forJob: entry.roster_job || job,
|
|
899
|
+
task: entry.task,
|
|
900
|
+
prompt,
|
|
901
|
+
worktreePath: entry.worktreePath,
|
|
902
|
+
root,
|
|
903
|
+
clock,
|
|
904
|
+
...(prepAsk ? { ask: prepAsk } : {}),
|
|
905
|
+
record: { member: entry.member || '' },
|
|
906
|
+
}));
|
|
907
|
+
}
|
|
908
|
+
return passes.get(prepJob);
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function dispatchEntryAttempts({
|
|
913
|
+
entry,
|
|
914
|
+
engine,
|
|
915
|
+
root,
|
|
916
|
+
dispatch,
|
|
917
|
+
installedEngines = null,
|
|
918
|
+
restaffState,
|
|
919
|
+
scoutAsk,
|
|
920
|
+
clock = Date.now,
|
|
921
|
+
defaultPin = null,
|
|
922
|
+
attempts = [],
|
|
923
|
+
job = 'build',
|
|
924
|
+
prepAsk = undefined,
|
|
765
925
|
}) {
|
|
766
926
|
const basePrompt = entry.prompt || buildFleetPrompt(entry.task, { worktreePath: entry.worktreePath, yolo: entry.yolo });
|
|
927
|
+
const prepFor = prepPassCache({ entry, root, clock, prepAsk, job });
|
|
767
928
|
let scoutPackPromise = null;
|
|
768
929
|
const scoutPack = () => {
|
|
769
930
|
if (scoutAsk === false) return Promise.resolve(null);
|
|
@@ -776,8 +937,12 @@ async function dispatchEntryWithRestaff({
|
|
|
776
937
|
}
|
|
777
938
|
return scoutPackPromise;
|
|
778
939
|
};
|
|
779
|
-
const runOnce = async (engineName) => {
|
|
780
|
-
const
|
|
940
|
+
const runOnce = async (engineName, pin = null) => {
|
|
941
|
+
const ranWith = pin || entry.roster_pin || (engineName === engine ? defaultPin : null) || {};
|
|
942
|
+
const scouted = appendVerifiedScoutPack(basePrompt, await scoutPack(), { worktreePath: entry.worktreePath });
|
|
943
|
+
// A worker whose roster line asks for prep gets its brief first.
|
|
944
|
+
const prep = ranWith.prep ? await prepFor(ranWith.prep, scouted) : null;
|
|
945
|
+
const prompt = withPrepBrief(scouted, prep);
|
|
781
946
|
const brief = captureDispatchBrief({
|
|
782
947
|
root,
|
|
783
948
|
task: entry.task,
|
|
@@ -788,24 +953,36 @@ async function dispatchEntryWithRestaff({
|
|
|
788
953
|
author: entry.author || '',
|
|
789
954
|
yolo: entry.yolo,
|
|
790
955
|
});
|
|
956
|
+
const startedMs = clock();
|
|
957
|
+
let runResult;
|
|
791
958
|
try {
|
|
792
|
-
|
|
959
|
+
runResult = normalizeDispatchResult(await dispatch({ ...entry, engine: engineName, prompt, brief_id: brief.brief_id, briefId: brief.brief_id, skipBriefCapture: true, ...(pin ? { roster_pin: pin } : {}) }), engineName);
|
|
793
960
|
if (!runResult.brief_id) runResult.brief_id = brief.brief_id;
|
|
794
|
-
|
|
961
|
+
if (pin && pin.maxSeconds && !runResult.max_seconds) runResult.max_seconds = pin.maxSeconds;
|
|
795
962
|
} catch (err) {
|
|
796
|
-
|
|
963
|
+
runResult = normalizeDispatchResult({
|
|
797
964
|
brief_id: brief.brief_id,
|
|
798
965
|
exitCode: 1,
|
|
799
966
|
report: '',
|
|
800
967
|
stderr: String(err && err.message || err),
|
|
801
968
|
}, engineName);
|
|
802
969
|
}
|
|
970
|
+
if (!runResult.max_seconds && ranWith.maxSeconds) runResult.max_seconds = ranWith.maxSeconds;
|
|
971
|
+
attempts.push(rosterAttemptRecord(engineName, ranWith, startedMs, clock(), runResult, prep));
|
|
972
|
+
return runResult;
|
|
803
973
|
};
|
|
804
974
|
|
|
805
975
|
const first = await runOnce(engine);
|
|
976
|
+
if (!first.max_seconds && entry.maxSeconds) first.max_seconds = entry.maxSeconds;
|
|
806
977
|
const deadEngine = detectDeadEngineDispatch(first);
|
|
807
978
|
recordDispatchEngineHealth(first, deadEngine, root);
|
|
808
979
|
if (!deadEngine) return first;
|
|
980
|
+
// A stall (time cap, watchdog, dropped connection) walks the roster's build
|
|
981
|
+
// team; with no roster line the one-shot restaff below runs as before.
|
|
982
|
+
const handover = deadEngine && engineStallReason(first)
|
|
983
|
+
? await handOverStalledDispatch({ entry, engine, first, root, runOnce })
|
|
984
|
+
: null;
|
|
985
|
+
if (handover) return handover;
|
|
809
986
|
stampDispatchBrief(root, first.brief_id, 'fail', `restaffed from ${engine}: ${deadEngine.reason}`);
|
|
810
987
|
|
|
811
988
|
const outage = { from: engine, reason: deadEngine.reason };
|
|
@@ -832,6 +1009,129 @@ async function dispatchEntryWithRestaff({
|
|
|
832
1009
|
};
|
|
833
1010
|
}
|
|
834
1011
|
|
|
1012
|
+
function stallLine(result, engineName) {
|
|
1013
|
+
const stall = engineStallReason(result);
|
|
1014
|
+
return `${engineName} ${stall ? stall.text : 'failed'}`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// A stalled run hands the same brief to the next ready worker on the build
|
|
1018
|
+
// roster, each with its own model, effort, and time cap, once per worker.
|
|
1019
|
+
// It stops at the first run that succeeds or fails for a real task reason,
|
|
1020
|
+
// or when the team runs out. The stalled engine is already benched, so the
|
|
1021
|
+
// team read here starts past it. Null when no roster line decides build.
|
|
1022
|
+
async function handOverStalledDispatch({ entry, engine, first, root, runOnce, job = 'build' }) {
|
|
1023
|
+
const tried = new Set([engine]);
|
|
1024
|
+
const reasons = [];
|
|
1025
|
+
const legs = [];
|
|
1026
|
+
let last = first;
|
|
1027
|
+
let lastEngine = engine;
|
|
1028
|
+
for (;;) {
|
|
1029
|
+
let team;
|
|
1030
|
+
try {
|
|
1031
|
+
team = resolveJobTeam(entry.roster_job || job, root, { lowStakes: false });
|
|
1032
|
+
} catch {
|
|
1033
|
+
team = null;
|
|
1034
|
+
}
|
|
1035
|
+
// Once every worker is benched the roster stops deciding; that is the
|
|
1036
|
+
// team running out, not a missing roster.
|
|
1037
|
+
const decided = Boolean(team && rosterDecided(team.source));
|
|
1038
|
+
const next = decided
|
|
1039
|
+
? team.team.find((worker) => worker && !tried.has(worker.id) && DISPATCH_CAPABLE.includes(worker.id))
|
|
1040
|
+
: null;
|
|
1041
|
+
if (!next) {
|
|
1042
|
+
if (!legs.length) return null;
|
|
1043
|
+
reasons.push(`${stallLine(last, lastEngine)}; no one left on the ${entry.roster_job || job} team`);
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
const line = stallLine(last, lastEngine);
|
|
1047
|
+
tried.add(next.id);
|
|
1048
|
+
reasons.push(`${line}; ${next.id} took over`);
|
|
1049
|
+
stampDispatchBrief(root, last.brief_id, 'fail', reasons[reasons.length - 1]);
|
|
1050
|
+
legs.push(failedDispatchLeg(last, lastEngine));
|
|
1051
|
+
// A backup preps only when its own line asks.
|
|
1052
|
+
const pin = {
|
|
1053
|
+
model: next.roster_model || '',
|
|
1054
|
+
effort: next.roster_effort || '',
|
|
1055
|
+
maxSeconds: capSeconds(next.roster_max_seconds),
|
|
1056
|
+
prep: next.roster_prep || '',
|
|
1057
|
+
};
|
|
1058
|
+
const result = await runOnce(next.id, pin);
|
|
1059
|
+
const dead = detectDeadEngineDispatch(result);
|
|
1060
|
+
recordDispatchEngineHealth(result, dead, root);
|
|
1061
|
+
last = result;
|
|
1062
|
+
lastEngine = next.id;
|
|
1063
|
+
if (!dead || !engineStallReason(result)) break;
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
...last,
|
|
1067
|
+
handover: {
|
|
1068
|
+
from: engine,
|
|
1069
|
+
to: lastEngine,
|
|
1070
|
+
reasons,
|
|
1071
|
+
failed_legs: legs,
|
|
1072
|
+
},
|
|
1073
|
+
handover_reason: reasons.join('. '),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Claude prints plain text by default, so a claude run had no tokens on its
|
|
1078
|
+
// record. Asked for its result as JSON, it prints one object with the same
|
|
1079
|
+
// final text plus usage and cost. Only the claude CLI itself gets the flag;
|
|
1080
|
+
// a custom runner template or a renamed runner keeps its plain launch.
|
|
1081
|
+
const CLAUDE_JSON_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
1082
|
+
|
|
1083
|
+
function claudeJsonLaunch(engine, command) {
|
|
1084
|
+
return CLAUDE_JSON_ENGINES.has(engine) && /^'?(?:[^'\s]*\/)?claude'? -p /.test(String(command || ''));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// One parsed claude result object from a line of stdout, or null when the
|
|
1088
|
+
// line is not one (plain text, partial json, some other event shape).
|
|
1089
|
+
function claudeResultEvent(line) {
|
|
1090
|
+
const text = String(line || '').trim();
|
|
1091
|
+
if (!text.startsWith('{')) return null;
|
|
1092
|
+
try {
|
|
1093
|
+
const event = JSON.parse(text);
|
|
1094
|
+
if (!event || typeof event !== 'object' || (event.type !== 'result' && typeof event.result !== 'string')) return null;
|
|
1095
|
+
return event;
|
|
1096
|
+
} catch {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// The report text for a claude result event. A run that ended in an error
|
|
1102
|
+
// carries no result string, only a subtype and an errors list; without this
|
|
1103
|
+
// the report and live log went blank on exactly the runs that needed their
|
|
1104
|
+
// error kept.
|
|
1105
|
+
function claudeResultText(event) {
|
|
1106
|
+
const result = typeof event.result === 'string' ? event.result : '';
|
|
1107
|
+
if (result.trim()) return result;
|
|
1108
|
+
const errors = (Array.isArray(event.errors) ? event.errors : [])
|
|
1109
|
+
.map((entry) => String(entry).trim())
|
|
1110
|
+
.filter(Boolean);
|
|
1111
|
+
if (event.is_error !== true && !errors.length) return result;
|
|
1112
|
+
return [String(event.subtype || '').trim(), errors.join('; ')].filter(Boolean).join(': ');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// The final text, usage, and cost from claude's JSON result, or null when
|
|
1116
|
+
// the output is not one (the run died first, or a stand-in printed text).
|
|
1117
|
+
// The result is the last line that parses as one, so plain lines the engine
|
|
1118
|
+
// printed earlier in the run do not hide it.
|
|
1119
|
+
function unwrapClaudeJsonResult(stdout) {
|
|
1120
|
+
const text = String(stdout || '').trim();
|
|
1121
|
+
if (!text) return null;
|
|
1122
|
+
const candidates = [text, ...text.split(/\r?\n/).reverse()];
|
|
1123
|
+
for (const candidate of candidates) {
|
|
1124
|
+
const event = claudeResultEvent(candidate);
|
|
1125
|
+
if (!event) continue;
|
|
1126
|
+
return {
|
|
1127
|
+
text: claudeResultText(event),
|
|
1128
|
+
...(event.usage && typeof event.usage === 'object' ? { usage: event.usage } : {}),
|
|
1129
|
+
...(typeof event.total_cost_usd === 'number' ? { cost_usd: event.total_cost_usd } : {}),
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
835
1135
|
// Run one engine on one task in one worktree. Blocking; the conductor runs
|
|
836
1136
|
// dispatches in parallel via child processes, not threads. `runner` is
|
|
837
1137
|
// injectable for tests. `prompt` is injectable too: a caller-supplied prompt
|
|
@@ -858,7 +1158,7 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
858
1158
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
859
1159
|
watchdogArtifact = path.join(artifactDir, `codex-watchdog-${task.display_id || 'task'}-${crypto.randomBytes(4).toString('hex')}.json`);
|
|
860
1160
|
}
|
|
861
|
-
|
|
1161
|
+
let command = buildEngineCommand(engine, promptFile, {
|
|
862
1162
|
yolo,
|
|
863
1163
|
sealed,
|
|
864
1164
|
allowedTools,
|
|
@@ -868,6 +1168,9 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
868
1168
|
effort,
|
|
869
1169
|
maxSeconds,
|
|
870
1170
|
});
|
|
1171
|
+
// Tokens for the run record only: the report stays claude's final text.
|
|
1172
|
+
const claudeJson = claudeJsonLaunch(engine, command);
|
|
1173
|
+
if (claudeJson) command = `${command} --output-format json`;
|
|
871
1174
|
// A roster time cap replaces the default backstop. Codex keeps a minute of
|
|
872
1175
|
// slack past its watchdog so the watchdog's own receipt gets written.
|
|
873
1176
|
const cap = capSeconds(maxSeconds);
|
|
@@ -875,6 +1178,7 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
875
1178
|
? cap * 1000 + (engine === 'codex' ? 60000 : 0)
|
|
876
1179
|
: (engine === 'codex' ? CODEX_DISPATCH_TIMEOUT_MS : DEFAULT_DISPATCH_TIMEOUT_MS);
|
|
877
1180
|
const dispatchTimeoutMs = timeoutMs === null || timeoutMs === undefined ? defaultTimeoutMs : timeoutMs;
|
|
1181
|
+
let flushLiveLogTail = null;
|
|
878
1182
|
const exec = runner || ((cmd) => {
|
|
879
1183
|
const childEnv = sealed && environment
|
|
880
1184
|
? { ...environment }
|
|
@@ -907,7 +1211,28 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
907
1211
|
};
|
|
908
1212
|
if (liveLogPath) {
|
|
909
1213
|
fs.mkdirSync(path.dirname(liveLogPath), { recursive: true });
|
|
910
|
-
|
|
1214
|
+
// Claude's result JSON is one line at the end of the stream: every
|
|
1215
|
+
// other stdout line goes to the live log as it lands so a watching
|
|
1216
|
+
// operator sees the run while it runs, and the held-back result line
|
|
1217
|
+
// is written as its final text once the run closes. A partial last
|
|
1218
|
+
// line waits in the tail until more output or the flush.
|
|
1219
|
+
let stdoutTail = '';
|
|
1220
|
+
spawnOptions.onOutputChunk = (chunk, stream) => {
|
|
1221
|
+
if (!claudeJson || stream !== 'stdout') {
|
|
1222
|
+
appendEngineLiveLogChunk(liveLogPath, chunk);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
stdoutTail += String(chunk);
|
|
1226
|
+
const lines = stdoutTail.split('\n');
|
|
1227
|
+
stdoutTail = lines.pop();
|
|
1228
|
+
const pass = lines.filter((line) => !claudeResultEvent(line));
|
|
1229
|
+
if (pass.length) appendEngineLiveLogChunk(liveLogPath, `${pass.join('\n')}\n`);
|
|
1230
|
+
};
|
|
1231
|
+
flushLiveLogTail = () => {
|
|
1232
|
+
const tail = stdoutTail;
|
|
1233
|
+
stdoutTail = '';
|
|
1234
|
+
if (tail && !claudeResultEvent(tail)) appendEngineLiveLogChunk(liveLogPath, tail);
|
|
1235
|
+
};
|
|
911
1236
|
}
|
|
912
1237
|
const run = sandboxProfile
|
|
913
1238
|
? runInReapedProcessGroup(executable, args, spawnOptions, cleanupControlDir, statusFile)
|
|
@@ -927,6 +1252,14 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
927
1252
|
if (watchdogArtifact && fs.existsSync(watchdogArtifact)) {
|
|
928
1253
|
try { watchdogReceipt = JSON.parse(fs.readFileSync(watchdogArtifact, 'utf8')); } catch {}
|
|
929
1254
|
}
|
|
1255
|
+
const unwrapped = claudeJson ? unwrapClaudeJsonResult(run.stdout) : null;
|
|
1256
|
+
const stdout = unwrapped ? unwrapped.text : String(run.stdout || '');
|
|
1257
|
+
if (flushLiveLogTail) {
|
|
1258
|
+
try { flushLiveLogTail(); } catch {}
|
|
1259
|
+
}
|
|
1260
|
+
if (claudeJson && liveLogPath && stdout) {
|
|
1261
|
+
try { appendEngineLiveLogChunk(liveLogPath, stdout); } catch {}
|
|
1262
|
+
}
|
|
930
1263
|
return {
|
|
931
1264
|
task: task.display_id || task.id,
|
|
932
1265
|
engine,
|
|
@@ -940,8 +1273,10 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
940
1273
|
signal: run.signal || null,
|
|
941
1274
|
timed_out: Boolean(run.error && run.error.code === 'ETIMEDOUT'),
|
|
942
1275
|
cancelled: Boolean(run.cancelled),
|
|
943
|
-
report:
|
|
1276
|
+
report: stdout.slice(-8000),
|
|
944
1277
|
stderr: String(run.stderr || '').slice(-2000),
|
|
1278
|
+
...(unwrapped && unwrapped.usage ? { usage: unwrapped.usage } : {}),
|
|
1279
|
+
...(unwrapped && unwrapped.cost_usd !== undefined ? { cost_usd: unwrapped.cost_usd } : {}),
|
|
945
1280
|
};
|
|
946
1281
|
};
|
|
947
1282
|
if (result && typeof result.then === 'function') return result.then(toDispatch);
|
|
@@ -1232,6 +1567,7 @@ module.exports = {
|
|
|
1232
1567
|
nextInstalledFleetEngine,
|
|
1233
1568
|
rankFleetEnginesDetailed,
|
|
1234
1569
|
dispatchToEngine,
|
|
1570
|
+
recordDispatchOutcome,
|
|
1235
1571
|
taskTags,
|
|
1236
1572
|
isHumanHoldTag,
|
|
1237
1573
|
isDecisionTask,
|
|
@@ -2204,7 +2540,23 @@ function reviewWorktreeSnapshot(worktreePath) {
|
|
|
2204
2540
|
};
|
|
2205
2541
|
}
|
|
2206
2542
|
|
|
2207
|
-
|
|
2543
|
+
// Every validator attempt records one review line, handed over when a credit
|
|
2544
|
+
// wall passes the review to the next validator.
|
|
2545
|
+
async function runIndependentValidator(options) {
|
|
2546
|
+
const attempts = [];
|
|
2547
|
+
const clock = typeof options.clock === 'function' ? options.clock : Date.now;
|
|
2548
|
+
try {
|
|
2549
|
+
return await validateWithAttempts({ ...options, clock, attempts });
|
|
2550
|
+
} finally {
|
|
2551
|
+
appendRosterAttempts(options.root || process.cwd(), attempts, {
|
|
2552
|
+
job: 'review',
|
|
2553
|
+
task: dispatchTaskId(options.task),
|
|
2554
|
+
source: 'one lap',
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
async function validateWithAttempts({
|
|
2208
2560
|
root,
|
|
2209
2561
|
task,
|
|
2210
2562
|
worktreePath,
|
|
@@ -2214,6 +2566,9 @@ async function runIndependentValidator({
|
|
|
2214
2566
|
validatorDispatcher = null,
|
|
2215
2567
|
stateInspector = reviewWorktreeSnapshot,
|
|
2216
2568
|
validatorModels = null,
|
|
2569
|
+
clock = Date.now,
|
|
2570
|
+
attempts = [],
|
|
2571
|
+
prepAsk = undefined,
|
|
2217
2572
|
}) {
|
|
2218
2573
|
const candidates = [...new Set((validatorEngines || []).map((value) => String(value || '').trim()).filter(Boolean))]
|
|
2219
2574
|
.filter((name) => RUNNER_PROFILE_DEFS[name] && name !== executorEngine);
|
|
@@ -2236,7 +2591,7 @@ async function runIndependentValidator({
|
|
|
2236
2591
|
};
|
|
2237
2592
|
}
|
|
2238
2593
|
const dispatch = validatorDispatcher || ((entry) => Promise.resolve(dispatchToEngine({
|
|
2239
|
-
...validatorPin(validatorModels, entry.engine),
|
|
2594
|
+
...pinEngineFlags(validatorPin(validatorModels, entry.engine)),
|
|
2240
2595
|
task,
|
|
2241
2596
|
engine: entry.engine,
|
|
2242
2597
|
worktreePath,
|
|
@@ -2252,9 +2607,20 @@ async function runIndependentValidator({
|
|
|
2252
2607
|
skipBriefCapture: true,
|
|
2253
2608
|
})));
|
|
2254
2609
|
|
|
2610
|
+
// One prep pass per prep job for this task: a backup reviewer whose line
|
|
2611
|
+
// asks for the same prep reuses the brief the first reviewer's prep wrote.
|
|
2612
|
+
const reviewPrompt = buildOneLapValidatorPrompt(task, { verifierCommand, executorEngine });
|
|
2613
|
+
const prepPasses = new Map();
|
|
2255
2614
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
2256
2615
|
const validatorEngine = candidates[index];
|
|
2257
|
-
const
|
|
2616
|
+
const pin = validatorPin(validatorModels, validatorEngine);
|
|
2617
|
+
// A reviewer whose roster line asks for prep reads a brief first; the
|
|
2618
|
+
// prep pass is read-only and runs before the worktree snapshot.
|
|
2619
|
+
if (pin.prep && !prepPasses.has(pin.prep)) {
|
|
2620
|
+
prepPasses.set(pin.prep, runPrepPass({ prepJob: pin.prep, forJob: 'review', task, prompt: reviewPrompt, worktreePath, root, clock, ...(prepAsk ? { ask: prepAsk } : {}) }));
|
|
2621
|
+
}
|
|
2622
|
+
const prep = pin.prep ? await prepPasses.get(pin.prep) : null;
|
|
2623
|
+
const prompt = withPrepBrief(reviewPrompt, prep);
|
|
2258
2624
|
const before = stateInspector(worktreePath);
|
|
2259
2625
|
if (!before || before.ok !== true) {
|
|
2260
2626
|
return {
|
|
@@ -2275,6 +2641,7 @@ async function runIndependentValidator({
|
|
|
2275
2641
|
};
|
|
2276
2642
|
}
|
|
2277
2643
|
let dispatched;
|
|
2644
|
+
const startedMs = clock();
|
|
2278
2645
|
try {
|
|
2279
2646
|
dispatched = normalizeDispatchResult(await dispatch({
|
|
2280
2647
|
task,
|
|
@@ -2285,6 +2652,8 @@ async function runIndependentValidator({
|
|
|
2285
2652
|
} catch (error) {
|
|
2286
2653
|
dispatched = normalizeDispatchResult({ exitCode: 1, stderr: error.message || String(error) }, validatorEngine);
|
|
2287
2654
|
}
|
|
2655
|
+
const attempt = rosterAttemptRecord(validatorEngine, pin, startedMs, clock(), dispatched, prep);
|
|
2656
|
+
attempts.push(attempt);
|
|
2288
2657
|
const after = stateInspector(worktreePath);
|
|
2289
2658
|
const output = dispatchResultOutput(dispatched).slice(-8000);
|
|
2290
2659
|
const verdictOutput = [dispatched.report, dispatched.stdout]
|
|
@@ -2306,6 +2675,12 @@ async function runIndependentValidator({
|
|
|
2306
2675
|
brief_id: dispatched.brief_id || null,
|
|
2307
2676
|
worktree_unchanged: unchanged,
|
|
2308
2677
|
};
|
|
2678
|
+
if (attempt.outcome === 'landed') {
|
|
2679
|
+
if (!unchanged) Object.assign(attempt, { outcome: 'failed', detail: 'changed the worktree it was reviewing' });
|
|
2680
|
+
else if (verdict.verdict === 'reject') attempt.detail = 'rejected the change';
|
|
2681
|
+
else if (verdict.passed) attempt.detail = 'signed off';
|
|
2682
|
+
else Object.assign(attempt, { outcome: 'failed', detail: 'gave no clear verdict' });
|
|
2683
|
+
}
|
|
2309
2684
|
if (!unchanged) return { ok: false, stage: 'validator_mutated_worktree', validator_result: validatorResult };
|
|
2310
2685
|
if (exitCode !== 0) {
|
|
2311
2686
|
const outage = detectDeadEngineDispatch(dispatched);
|
|
@@ -2620,6 +2995,17 @@ function defaultSelfLandCheck({ worktreePath, targetRef = DISPATCH_SELF_LAND_TAR
|
|
|
2620
2995
|
// One flight. Staff -> dispatch in parallel -> land serially -> receipt.
|
|
2621
2996
|
// No fleet state file: progress is narrated via `log`, durability lives in
|
|
2622
2997
|
// worktrees, task claims, and the receipt written at the end.
|
|
2998
|
+
// Fleet builds run on each engine's own defaults; the one thing the build
|
|
2999
|
+
// line adds is its prep job, when the line for this same engine asks for one.
|
|
3000
|
+
function fleetPrepPin(engineName, root) {
|
|
3001
|
+
try {
|
|
3002
|
+
const pin = rosterPinFor('executor', engineName, root);
|
|
3003
|
+
return pin.roster_prep ? { prep: pin.roster_prep } : null;
|
|
3004
|
+
} catch {
|
|
3005
|
+
return null;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2623
3009
|
async function runFleetFlight({
|
|
2624
3010
|
root = process.cwd(),
|
|
2625
3011
|
slots = 3,
|
|
@@ -2633,6 +3019,8 @@ async function runFleetFlight({
|
|
|
2633
3019
|
checkoutBase = 'origin/master',
|
|
2634
3020
|
guardCliLink = guardGlobalCliLink,
|
|
2635
3021
|
scoutAsk = null,
|
|
3022
|
+
clock = Date.now,
|
|
3023
|
+
prepAsk = undefined,
|
|
2636
3024
|
} = {}) {
|
|
2637
3025
|
const cli = ownCli || defaultOwnCli(root);
|
|
2638
3026
|
// Staff first, rank second: every task staffFlight returns already cleared
|
|
@@ -2694,6 +3082,7 @@ async function runFleetFlight({
|
|
|
2694
3082
|
briefId: entry.brief_id,
|
|
2695
3083
|
skipBriefCapture: true,
|
|
2696
3084
|
liveLogPath,
|
|
3085
|
+
...(entry.roster_pin ? { model: entry.roster_pin.model, effort: entry.roster_pin.effort, maxSeconds: entry.roster_pin.maxSeconds } : {}),
|
|
2697
3086
|
}));
|
|
2698
3087
|
}));
|
|
2699
3088
|
const restaffState = { used: false };
|
|
@@ -2733,12 +3122,16 @@ async function runFleetFlight({
|
|
|
2733
3122
|
installedEngines: roster,
|
|
2734
3123
|
restaffState,
|
|
2735
3124
|
scoutAsk: scoutAsk === null && dispatcher ? false : (scoutAsk || undefined),
|
|
3125
|
+
clock,
|
|
3126
|
+
defaultPin: fleetPrepPin(entry.engine, root),
|
|
3127
|
+
prepAsk,
|
|
2736
3128
|
}).then((result) => ({ entry, result }))
|
|
2737
3129
|
));
|
|
2738
3130
|
flight.results = results.map(({ entry, result }) => {
|
|
2739
3131
|
const row = { task: entry.task.display_id, engine: result.engine || entry.engine, exitCode: result.exitCode };
|
|
2740
3132
|
if (result.brief_id) row.brief_id = result.brief_id;
|
|
2741
3133
|
if (result.restaffed) row.restaffed = result.restaffed;
|
|
3134
|
+
if (result.handover) row.handover = result.handover;
|
|
2742
3135
|
if (result.deadEngine) row.deadEngine = result.deadEngine;
|
|
2743
3136
|
return row;
|
|
2744
3137
|
});
|
|
@@ -2760,6 +3153,7 @@ async function runFleetFlight({
|
|
|
2760
3153
|
const activeEngine = result.engine || entry.engine;
|
|
2761
3154
|
const landingEntry = { ...entry, engine: activeEngine };
|
|
2762
3155
|
if (result.restaffed) log(` restaffed ${entry.task.display_id}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
|
|
3156
|
+
if (result.handover) log(` handed over ${entry.task.display_id}: ${result.handover_reason}`);
|
|
2763
3157
|
const buildFailure = detectDeadEngineDispatch(result);
|
|
2764
3158
|
if (buildFailure) {
|
|
2765
3159
|
const paused = {
|
|
@@ -2770,6 +3164,7 @@ async function runFleetFlight({
|
|
|
2770
3164
|
detail: String(result.stderr || buildFailure.reason).slice(0, 200),
|
|
2771
3165
|
};
|
|
2772
3166
|
if (result.restaffed) paused.restaffed = result.restaffed;
|
|
3167
|
+
if (result.handover) paused.handover = result.handover;
|
|
2773
3168
|
if (result.deadEngine) paused.deadEngine = result.deadEngine;
|
|
2774
3169
|
flight.paused.push(paused);
|
|
2775
3170
|
stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.task.display_id}`);
|
|
@@ -2824,6 +3219,7 @@ async function runFleetFlight({
|
|
|
2824
3219
|
log,
|
|
2825
3220
|
});
|
|
2826
3221
|
|
|
3222
|
+
recordFlightRosterRuns(root, flight, results, { job: 'build', source: 'fleet' });
|
|
2827
3223
|
flight.status = flight.paused.length ? 'failed' : 'completed';
|
|
2828
3224
|
flight.finished_at = new Date().toISOString();
|
|
2829
3225
|
atomicWriteFlightReceipt(flight, receiptPath);
|
|
@@ -2920,6 +3316,13 @@ async function runDispatchFlight({
|
|
|
2920
3316
|
effort = '',
|
|
2921
3317
|
maxSeconds = 0,
|
|
2922
3318
|
validatorModels = null,
|
|
3319
|
+
// The roster job this dispatch runs for, and the clock its run record uses.
|
|
3320
|
+
rosterJob = 'build',
|
|
3321
|
+
clock = Date.now,
|
|
3322
|
+
// The job whose lead writes the requested engine a brief first ("prep:
|
|
3323
|
+
// search" on its roster line), and an injectable ask for that prep pass.
|
|
3324
|
+
prep = '',
|
|
3325
|
+
prepAsk = undefined,
|
|
2923
3326
|
} = {}) {
|
|
2924
3327
|
if (!engine) throw new Error('runDispatchFlight: engine is required');
|
|
2925
3328
|
if (!DISPATCH_CAPABLE.includes(engine)) {
|
|
@@ -3046,6 +3449,7 @@ async function runDispatchFlight({
|
|
|
3046
3449
|
remoteBoundary,
|
|
3047
3450
|
remoteMasterBefore: remoteBoundary ? remoteBoundary.protectedMaster : '',
|
|
3048
3451
|
startCommit,
|
|
3452
|
+
...(maxSeconds ? { maxSeconds } : {}),
|
|
3049
3453
|
...(trustedPrompt ? { prompt: trustedPrompt } : {}),
|
|
3050
3454
|
});
|
|
3051
3455
|
log(` building ${taskId} in ${path.basename(worktreePath)}`);
|
|
@@ -3077,9 +3481,9 @@ async function runDispatchFlight({
|
|
|
3077
3481
|
briefId: entry.brief_id,
|
|
3078
3482
|
skipBriefCapture: true,
|
|
3079
3483
|
liveLogPath,
|
|
3080
|
-
model: entry.engine === engine ? model : '',
|
|
3081
|
-
effort: entry.engine === engine ? effort : '',
|
|
3082
|
-
maxSeconds: entry.engine === engine ? maxSeconds : 0,
|
|
3484
|
+
model: entry.roster_pin ? entry.roster_pin.model : (entry.engine === engine ? model : ''),
|
|
3485
|
+
effort: entry.roster_pin ? entry.roster_pin.effort : (entry.engine === engine ? effort : ''),
|
|
3486
|
+
maxSeconds: entry.roster_pin ? entry.roster_pin.maxSeconds : (entry.engine === engine ? maxSeconds : 0),
|
|
3083
3487
|
}));
|
|
3084
3488
|
}));
|
|
3085
3489
|
const restaffState = { used: false };
|
|
@@ -3098,6 +3502,10 @@ async function runDispatchFlight({
|
|
|
3098
3502
|
installedEngines,
|
|
3099
3503
|
restaffState,
|
|
3100
3504
|
scoutAsk: scoutAsk === null && dispatcher ? false : (scoutAsk || undefined),
|
|
3505
|
+
clock,
|
|
3506
|
+
defaultPin: { model, effort, maxSeconds: capSeconds(maxSeconds), ...(prep ? { prep } : {}) },
|
|
3507
|
+
job: rosterJob,
|
|
3508
|
+
prepAsk,
|
|
3101
3509
|
}).then((result) => {
|
|
3102
3510
|
const completedAtMs = Date.now();
|
|
3103
3511
|
return {
|
|
@@ -3124,6 +3532,7 @@ async function runDispatchFlight({
|
|
|
3124
3532
|
if (entry.startCommit) row.start_commit = entry.startCommit;
|
|
3125
3533
|
if (result.brief_id) row.brief_id = result.brief_id;
|
|
3126
3534
|
if (result.restaffed) row.restaffed = result.restaffed;
|
|
3535
|
+
if (result.handover) row.handover = result.handover;
|
|
3127
3536
|
if (result.deadEngine) row.deadEngine = result.deadEngine;
|
|
3128
3537
|
if (candidate && candidate.ok) {
|
|
3129
3538
|
row.candidate_commit = candidate.commit;
|
|
@@ -3216,6 +3625,7 @@ async function runDispatchFlight({
|
|
|
3216
3625
|
const readyActor = explicitActor || `fleet-${activeEngine}`;
|
|
3217
3626
|
const landingEntry = { ...entry, engine: activeEngine, candidate };
|
|
3218
3627
|
if (result.restaffed) log(` restaffed ${entry.taskId}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
|
|
3628
|
+
if (result.handover) log(` handed over ${entry.taskId}: ${result.handover_reason}`);
|
|
3219
3629
|
const buildFailure = detectDeadEngineDispatch(result);
|
|
3220
3630
|
if (buildFailure) {
|
|
3221
3631
|
const paused = {
|
|
@@ -3227,6 +3637,7 @@ async function runDispatchFlight({
|
|
|
3227
3637
|
worktree: entry.worktreePath,
|
|
3228
3638
|
};
|
|
3229
3639
|
if (result.restaffed) paused.restaffed = result.restaffed;
|
|
3640
|
+
if (result.handover) paused.handover = result.handover;
|
|
3230
3641
|
if (result.deadEngine) paused.deadEngine = result.deadEngine;
|
|
3231
3642
|
flight.paused.push(paused);
|
|
3232
3643
|
stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.taskId}`);
|
|
@@ -3368,6 +3779,8 @@ async function runDispatchFlight({
|
|
|
3368
3779
|
validatorDispatcher,
|
|
3369
3780
|
stateInspector: validatorStateInspector || reviewWorktreeSnapshot,
|
|
3370
3781
|
validatorModels,
|
|
3782
|
+
clock,
|
|
3783
|
+
prepAsk,
|
|
3371
3784
|
});
|
|
3372
3785
|
validatorResult = validation.validator_result;
|
|
3373
3786
|
flight.validator_result = validatorResult;
|
|
@@ -3612,6 +4025,11 @@ async function runDispatchFlight({
|
|
|
3612
4025
|
log,
|
|
3613
4026
|
});
|
|
3614
4027
|
|
|
4028
|
+
recordFlightRosterRuns(root, flight, results, {
|
|
4029
|
+
job: rosterJob,
|
|
4030
|
+
member: /^fleet-/.test(explicitActor) ? '' : explicitActor,
|
|
4031
|
+
source: flight.context && flight.context.source === 'one_lap' ? 'one lap' : 'dispatch',
|
|
4032
|
+
});
|
|
3615
4033
|
flight.status = flight.paused.length ? 'failed' : 'completed';
|
|
3616
4034
|
flight.finished_at = new Date().toISOString();
|
|
3617
4035
|
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|