atris 3.68.2 → 3.68.3
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/engines/SKILL.md +1 -1
- package/atris/team/customer-lead/MEMBER.md +1 -1
- package/atris/team/executor/MEMBER.md +19 -18
- package/atris/team/executor/skills/isolated-slice-builder/SKILL.md +21 -0
- package/atris/team/navigator/MEMBER.md +3 -1
- package/atris/team/navigator/skills/loophole-proof-planner/SKILL.md +20 -0
- package/atris/team/researcher/MEMBER.md +2 -1
- package/atris/team/researcher/skills/grounded-evidence-brief/SKILL.md +19 -0
- package/atris/team/validator/MEMBER.md +2 -7
- package/atris/team/validator/skills/cross-model-referee/SKILL.md +20 -0
- package/commands/autoland.js +2 -1
- package/commands/engine.js +2 -2
- package/commands/goal.js +61 -2
- package/commands/land.js +34 -7
- package/commands/member.js +1542 -41
- package/commands/mission.js +506 -18
- package/commands/task.js +26 -3
- package/commands/worktree.js +144 -34
- package/lib/auto-accept-certified.js +7 -6
- package/lib/codex-flight.js +6 -12
- package/lib/conductor-artifacts.js +3 -1
- package/lib/default-runner.js +42 -7
- package/lib/engine-ask.js +8 -1
- package/lib/engine-registry.js +17 -0
- package/lib/fleet.js +163 -88
- package/lib/member-alive.js +1 -1
- package/lib/member-approval.js +153 -0
- package/lib/mission-task-gate.js +170 -0
- package/lib/one-lap-validator.js +2 -1
- package/lib/roster-runs.js +28 -0
- package/lib/runner-command.js +32 -8
- package/lib/task-db.js +25 -1
- package/package.json +5 -1
- package/scripts/det/ytnotes +49 -10
package/lib/fleet.js
CHANGED
|
@@ -32,7 +32,7 @@ const {
|
|
|
32
32
|
buildOneLapValidatorPrompt,
|
|
33
33
|
parseOneLapValidatorVerdict,
|
|
34
34
|
} = require('./one-lap-validator');
|
|
35
|
-
const { listWorktrees, defaultMainlineBase } = require('../commands/worktree');
|
|
35
|
+
const { listWorktrees, defaultMainlineBase, gitFailureDetail, gitNetworkEnvironment } = require('../commands/worktree');
|
|
36
36
|
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
37
37
|
const { matchLessons } = require('./lesson-preflight');
|
|
38
38
|
const { sleepSync } = require('./sleep-sync');
|
|
@@ -171,20 +171,34 @@ const YOLO_ENGINE_FLAGS = Object.freeze({
|
|
|
171
171
|
codex: '--dangerously-bypass-approvals-and-sandbox',
|
|
172
172
|
claude: '--dangerously-skip-permissions',
|
|
173
173
|
});
|
|
174
|
-
const DISPATCH_SELF_LAND_TARGET = '
|
|
174
|
+
const DISPATCH_SELF_LAND_TARGET = 'HEAD';
|
|
175
175
|
|
|
176
176
|
// Dispatch used to cut every build worktree from origin/master and check
|
|
177
177
|
// self-landing against it, hard-coded. A workspace whose protected branch is
|
|
178
178
|
// `main` had no origin/master, so the worktree cut and the ancestry check both
|
|
179
|
-
// failed. Resolve the workspace's real protected branch instead
|
|
180
|
-
//
|
|
181
|
-
|
|
179
|
+
// failed. Resolve the workspace's real protected branch instead, using local
|
|
180
|
+
// HEAD only when origin is absent.
|
|
181
|
+
|
|
182
|
+
// Only an absent origin enables local dispatch. An unreachable origin is
|
|
183
|
+
// still an origin and must keep the protected-branch path unchanged.
|
|
184
|
+
function localDispatchBase(root) {
|
|
185
|
+
const remote = spawnSync('git', ['remote'], { cwd: root, encoding: 'utf8' });
|
|
186
|
+
if (remote.status !== 0 || String(remote.stdout || '').trim().split(/\s+/).includes('origin')) return '';
|
|
187
|
+
const head = spawnSync('git', ['rev-parse', '--verify', 'HEAD^{commit}'], { cwd: root, encoding: 'utf8' });
|
|
188
|
+
return head.status === 0 ? String(head.stdout || '').trim() : '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Without origin, pin the current local commit even if stale remote refs remain.
|
|
182
192
|
function resolveDispatchLandTarget(root, explicitBase) {
|
|
183
193
|
if (explicitBase) return explicitBase;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
const localBase = localDispatchBase(root);
|
|
195
|
+
if (localBase) return localBase;
|
|
196
|
+
const mainline = defaultMainlineBase(root);
|
|
197
|
+
if (mainline && mainline !== 'HEAD') return mainline;
|
|
198
|
+
// A configured origin with no known mainline must fail at worktree start,
|
|
199
|
+
// before an engine can build from an unprotected local HEAD.
|
|
200
|
+
const origin = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: root, encoding: 'utf8' });
|
|
201
|
+
if (origin.status === 0) return 'origin/HEAD';
|
|
188
202
|
return DISPATCH_SELF_LAND_TARGET;
|
|
189
203
|
}
|
|
190
204
|
const DEFAULT_DISPATCH_TIMEOUT_MS = 900000;
|
|
@@ -634,7 +648,10 @@ function detectDeadEngineDispatch(result) {
|
|
|
634
648
|
const exitCode = terminal.exit_code;
|
|
635
649
|
const signal = dispatchResultSignal(result);
|
|
636
650
|
if (signal) return { reason: 'signalled', signal, exitCode };
|
|
637
|
-
|
|
651
|
+
// A finished build's report and tool output are work, not engine failures.
|
|
652
|
+
const hasWorkOutput = [result && result.report, result && result.stdout]
|
|
653
|
+
.some((value) => String(value || '').trim());
|
|
654
|
+
if (terminal.reason === 'ok' && hasWorkOutput && !result.error && result.ok !== false) return null;
|
|
638
655
|
// A credit wall is read from engine-level signals only. The report and
|
|
639
656
|
// stdout are the task's own work: a failing "rate limit" test says nothing
|
|
640
657
|
// about the engine's credits.
|
|
@@ -651,6 +668,17 @@ function detectDeadEngineDispatch(result) {
|
|
|
651
668
|
? DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => engineText.includes(p))
|
|
652
669
|
: null;
|
|
653
670
|
if (pattern) return { reason: 'usage_limit', pattern };
|
|
671
|
+
// Engines print their own refusal at startup. Later stderr can contain a
|
|
672
|
+
// tool transcript, including the task's login checks, so stop at that boundary.
|
|
673
|
+
const startup = String(result && result.stderr || '').split(/\r?\n/).slice(0, 5);
|
|
674
|
+
const toolBoundary = startup.findIndex(line => /^(?:exec|tool\b|.*\bsucceeded in\b)/i.test(line.trim()));
|
|
675
|
+
const refusal = [
|
|
676
|
+
result && result.error && (result.error.message || result.error.code || result.error),
|
|
677
|
+
...startup.slice(0, toolBoundary < 0 ? startup.length : toolBoundary),
|
|
678
|
+
].filter(Boolean).map(value => String(value).trim())
|
|
679
|
+
.find(line => /^(?:error:\s*)?(?:refusing to run\b|(?:you are |you'?re )?not (?:logged in|authenticated)\b|please (?:log|sign) in\b|(?:login|authentication) required\b)/i.test(line));
|
|
680
|
+
if (refusal) return { reason: 'engine_refused', detail: refusal, exitCode };
|
|
681
|
+
if (terminal.reason === 'ok') return null;
|
|
654
682
|
if (terminal.reason === 'no_output' || terminal.reason === 'unknown' || terminal.reason === 'timeout' || terminal.reason === 'cancelled') {
|
|
655
683
|
return { reason: terminal.reason, exitCode };
|
|
656
684
|
}
|
|
@@ -660,6 +688,7 @@ function detectDeadEngineDispatch(result) {
|
|
|
660
688
|
function plainDispatchFailureCause(result, failure = {}) {
|
|
661
689
|
const reason = String(failure.reason || failure.stage || '').trim();
|
|
662
690
|
const output = dispatchResultOutput(result);
|
|
691
|
+
if (reason === 'engine_refused') return failure.detail;
|
|
663
692
|
if (reason === 'no_output') return 'the engine returned no output';
|
|
664
693
|
if (/not authenticated|please log in|login required|auth(?:entication)?[ _-]?expired/i.test(output)) {
|
|
665
694
|
return 'the engine login expired';
|
|
@@ -957,7 +986,7 @@ async function dispatchEntryAttempts({
|
|
|
957
986
|
if (handover) return handover;
|
|
958
987
|
stampDispatchBrief(root, first.brief_id, 'fail', `restaffed from ${engine}: ${deadEngine.reason}`);
|
|
959
988
|
|
|
960
|
-
const outage = { from: engine, reason: deadEngine.reason };
|
|
989
|
+
const outage = { from: engine, reason: deadEngine.reason, ...(deadEngine.detail ? { detail: deadEngine.detail } : {}) };
|
|
961
990
|
if (restaffState.used) {
|
|
962
991
|
return { ...first, deadEngine: { ...outage, skipped: 'already_restaffed' } };
|
|
963
992
|
}
|
|
@@ -976,6 +1005,7 @@ async function dispatchEntryAttempts({
|
|
|
976
1005
|
from: engine,
|
|
977
1006
|
to: fallback,
|
|
978
1007
|
reason: deadEngine.reason,
|
|
1008
|
+
...(deadEngine.detail ? { detail: deadEngine.detail } : {}),
|
|
979
1009
|
failed_legs: [failedDispatchLeg(first, engine)],
|
|
980
1010
|
},
|
|
981
1011
|
};
|
|
@@ -1362,7 +1392,7 @@ function dispatchResultToTerminal(entry = {}, result = {}) {
|
|
|
1362
1392
|
return payload;
|
|
1363
1393
|
}
|
|
1364
1394
|
|
|
1365
|
-
// Fleet landings always target
|
|
1395
|
+
// Fleet landings always target the repository mainline. Without the explicit --target, ship
|
|
1366
1396
|
// falls back to the launcher branch's atris-base, a flight launched from a
|
|
1367
1397
|
// feature-branch checkout would merge PRs into that branch while the receipt
|
|
1368
1398
|
// and land board define "landed" as in-master (PRs #207/#208, 2026-07-04).
|
|
@@ -1389,12 +1419,12 @@ function shipFailureDetail(shipped, { head = 400, tail = 500 } = {}) {
|
|
|
1389
1419
|
return `${cause}${headTail(combined.trim(), head, tail)}`;
|
|
1390
1420
|
}
|
|
1391
1421
|
|
|
1392
|
-
function fleetShipArgs(entry, check) {
|
|
1422
|
+
function fleetShipArgs(entry, check, targetRef = resolveDispatchLandTarget(entry.worktreePath || process.cwd())) {
|
|
1393
1423
|
return [
|
|
1394
1424
|
'worktree', 'ship',
|
|
1395
1425
|
'--message', `${String(entry.task.title || '').split(/[.:]/)[0].slice(0, 90)} (${entry.task.display_id}, built by ${entry.engine})`,
|
|
1396
1426
|
'--verify', check,
|
|
1397
|
-
'--target',
|
|
1427
|
+
'--target', targetRef,
|
|
1398
1428
|
'--merge',
|
|
1399
1429
|
];
|
|
1400
1430
|
}
|
|
@@ -1482,11 +1512,32 @@ function pausedPathsSuffix(landed) {
|
|
|
1482
1512
|
return paths.length ? ` (${paths.join(', ')})` : '';
|
|
1483
1513
|
}
|
|
1484
1514
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1515
|
+
// Probe the SSH setup once per runner, not once per git call.
|
|
1516
|
+
function boundedGit(cwd) {
|
|
1517
|
+
const env = gitNetworkEnvironment(process.env, cwd);
|
|
1518
|
+
return (args) => spawnSync('git', args, { cwd, encoding: 'utf8', env, timeout: 15000, killSignal: 'SIGKILL' });
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function landArrival({ worktreePath, targetRef = resolveDispatchLandTarget(worktreePath), git = null }) {
|
|
1522
|
+
const run = git || boundedGit(worktreePath);
|
|
1523
|
+
const remotes = run(['remote']);
|
|
1524
|
+
if (remotes.status !== 0) return { ok: false, stage: 'fetch', detail: String(remotes.stderr || '').trim() };
|
|
1525
|
+
if (String(remotes.stdout || '').trim().split(/\s+/).includes('origin')) {
|
|
1526
|
+
const fetch = run(['fetch', 'origin']);
|
|
1527
|
+
if (fetch.status !== 0) return { ok: false, stage: 'fetch', detail: gitFailureDetail(fetch) };
|
|
1528
|
+
}
|
|
1529
|
+
const sourceDirt = () => {
|
|
1530
|
+
const status = run(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1531
|
+
if (status.status !== 0) return { ok: false, stage: 'status', detail: String(status.stderr || '').trim() };
|
|
1532
|
+
const dirty = String(status.stdout || '').split('\n').filter(Boolean)
|
|
1533
|
+
.filter((line) => !isConductorStatusLine(line)).map((line) => line.slice(3).trim());
|
|
1534
|
+
return dirty.length ? { ok: false, stage: 'dirty_worktree', dirty } : null;
|
|
1535
|
+
};
|
|
1536
|
+
// Untracked source would not stop git rebase, but must never get swept into
|
|
1537
|
+
// a ship commit. Untracked receipts and ignored files are not source dirt.
|
|
1538
|
+
const beforeRebase = sourceDirt();
|
|
1539
|
+
if (beforeRebase) return beforeRebase;
|
|
1540
|
+
const rebase = run(['rebase', targetRef]);
|
|
1490
1541
|
if (rebase.status !== 0) {
|
|
1491
1542
|
const conflicts = String(run(['diff', '--name-only', '--diff-filter=U']).stdout || '')
|
|
1492
1543
|
.trim().split('\n').filter(Boolean);
|
|
@@ -1499,11 +1550,7 @@ function landArrival({ worktreePath, git = null }) {
|
|
|
1499
1550
|
// after a killed engine left modified/untracked files behind). An empty
|
|
1500
1551
|
// conflicts list is never a conflict: name the dirty paths instead.
|
|
1501
1552
|
if (!conflicts.length) {
|
|
1502
|
-
|
|
1503
|
-
.split('\n')
|
|
1504
|
-
.map((line) => line.slice(3).trim())
|
|
1505
|
-
.filter(Boolean);
|
|
1506
|
-
return { ok: false, stage: 'dirty_worktree', dirty };
|
|
1553
|
+
return sourceDirt() || { ok: false, stage: 'rebase_failed', detail: String(rebase.stderr || rebase.stdout || '').trim() };
|
|
1507
1554
|
}
|
|
1508
1555
|
return { ok: false, stage: 'rebase_conflict', conflicts };
|
|
1509
1556
|
}
|
|
@@ -1844,8 +1891,8 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
|
|
|
1844
1891
|
return environment;
|
|
1845
1892
|
}
|
|
1846
1893
|
|
|
1847
|
-
function conductorGitEnvironment() {
|
|
1848
|
-
const env =
|
|
1894
|
+
function conductorGitEnvironment(cwd) {
|
|
1895
|
+
const env = gitNetworkEnvironment(process.env, cwd);
|
|
1849
1896
|
for (const key of Object.keys(env)) {
|
|
1850
1897
|
if (/^GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)$/.test(key)) delete env[key];
|
|
1851
1898
|
}
|
|
@@ -1856,12 +1903,13 @@ function conductorGitEnvironment() {
|
|
|
1856
1903
|
return env;
|
|
1857
1904
|
}
|
|
1858
1905
|
|
|
1859
|
-
function remoteMasterOid(worktreePath, remote = 'origin') {
|
|
1860
|
-
const result = spawnSync('git', ['ls-remote', '--exit-code', remote,
|
|
1906
|
+
function remoteMasterOid(worktreePath, remote = 'origin', branch) {
|
|
1907
|
+
const result = spawnSync('git', ['ls-remote', '--exit-code', remote, `refs/heads/${branch}`], {
|
|
1861
1908
|
cwd: worktreePath,
|
|
1862
1909
|
encoding: 'utf8',
|
|
1863
1910
|
timeout: 15000,
|
|
1864
|
-
env: conductorGitEnvironment(),
|
|
1911
|
+
env: conductorGitEnvironment(worktreePath),
|
|
1912
|
+
killSignal: 'SIGKILL',
|
|
1865
1913
|
});
|
|
1866
1914
|
if (result.status !== 0) return '';
|
|
1867
1915
|
return String(result.stdout || '').trim().split(/\s+/)[0] || '';
|
|
@@ -1872,7 +1920,8 @@ function remoteRefsDigest(worktreePath, remote = 'origin') {
|
|
|
1872
1920
|
cwd: worktreePath,
|
|
1873
1921
|
encoding: 'utf8',
|
|
1874
1922
|
timeout: 30000,
|
|
1875
|
-
env: conductorGitEnvironment(),
|
|
1923
|
+
env: conductorGitEnvironment(worktreePath),
|
|
1924
|
+
killSignal: 'SIGKILL',
|
|
1876
1925
|
});
|
|
1877
1926
|
if (result.status !== 0) return '';
|
|
1878
1927
|
return crypto.createHash('sha256').update(String(result.stdout || '')).digest('hex');
|
|
@@ -1924,7 +1973,7 @@ function bareRefsDigest(gitDir) {
|
|
|
1924
1973
|
return crypto.createHash('sha256').update(String(result.stdout || '')).digest('hex');
|
|
1925
1974
|
}
|
|
1926
1975
|
|
|
1927
|
-
function prepareReviewSandbox({ root, taskId, engine }) {
|
|
1976
|
+
function prepareReviewSandbox({ root, taskId, engine, baseRef = '' }) {
|
|
1928
1977
|
const run = (args, options = {}) => spawnSync('git', args, {
|
|
1929
1978
|
cwd: options.cwd || root,
|
|
1930
1979
|
encoding: 'utf8',
|
|
@@ -1934,7 +1983,8 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
1934
1983
|
const runConductorGit = (args) => spawnSync('git', args, {
|
|
1935
1984
|
cwd: root,
|
|
1936
1985
|
encoding: 'utf8',
|
|
1937
|
-
env: conductorGitEnvironment(),
|
|
1986
|
+
env: conductorGitEnvironment(root),
|
|
1987
|
+
killSignal: 'SIGKILL',
|
|
1938
1988
|
timeout: 30000,
|
|
1939
1989
|
});
|
|
1940
1990
|
// `git` walks up to the nearest enclosing repository when `root` is not itself
|
|
@@ -1946,18 +1996,20 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
1946
1996
|
|| realpathOrResolve(String(toplevel.stdout || '').trim()) !== realpathOrResolve(root)) {
|
|
1947
1997
|
return { ok: false, detail: 'review-only dispatch requires the workspace to be its own git repository' };
|
|
1948
1998
|
}
|
|
1949
|
-
|
|
1999
|
+
baseRef = baseRef || resolveDispatchLandTarget(root);
|
|
2000
|
+
const mainBranch = baseRef.replace(/^origin\//, '');
|
|
2001
|
+
const fetched = runConductorGit(['fetch', 'origin', mainBranch]);
|
|
1950
2002
|
const original = runConductorGit(['remote', 'get-url', 'origin']);
|
|
1951
|
-
const base = run(['rev-parse', '--verify',
|
|
2003
|
+
const base = run(['rev-parse', '--verify', `${baseRef}^{commit}`]);
|
|
1952
2004
|
if (fetched.status !== 0 || original.status !== 0 || base.status !== 0) {
|
|
1953
|
-
return { ok: false, detail:
|
|
2005
|
+
return { ok: false, detail: `review-only dispatch requires a current protected ${baseRef}` };
|
|
1954
2006
|
}
|
|
1955
2007
|
const originalUrl = String(original.stdout || '').trim();
|
|
1956
|
-
const protectedMaster = remoteMasterOid(root, originalUrl);
|
|
2008
|
+
const protectedMaster = remoteMasterOid(root, originalUrl, mainBranch);
|
|
1957
2009
|
const protectedRefs = remoteRefsDigest(root, originalUrl);
|
|
1958
2010
|
const baseOid = String(base.stdout || '').trim();
|
|
1959
2011
|
if (!protectedMaster || !protectedRefs || baseOid !== protectedMaster) {
|
|
1960
|
-
return { ok: false, detail:
|
|
2012
|
+
return { ok: false, detail: `review-only ${baseRef} is not at the protected remote snapshot` };
|
|
1961
2013
|
}
|
|
1962
2014
|
|
|
1963
2015
|
const token = `${String(taskId || 'task').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'task'}-${process.pid}-${crypto.randomBytes(5).toString('hex')}`;
|
|
@@ -1978,20 +2030,20 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
1978
2030
|
|
|
1979
2031
|
try {
|
|
1980
2032
|
fs.mkdirSync(sandboxRoot, { recursive: true });
|
|
1981
|
-
let result = run(['bundle', 'create', seedBundle,
|
|
2033
|
+
let result = run(['bundle', 'create', seedBundle, baseRef]);
|
|
1982
2034
|
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not create seed bundle').trim());
|
|
1983
|
-
result = run(['init', '--bare',
|
|
2035
|
+
result = run(['init', '--bare', `--initial-branch=${mainBranch}`, gitDir], { cwd: sandboxRoot });
|
|
1984
2036
|
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not initialize sandbox object store').trim());
|
|
1985
2037
|
result = spawnSync('git', ['--git-dir', gitDir, 'bundle', 'unbundle', seedBundle], { encoding: 'utf8', timeout: 30000 });
|
|
1986
2038
|
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not seed sandbox object store').trim());
|
|
1987
|
-
result = run(['init', '--bare',
|
|
2039
|
+
result = run(['init', '--bare', `--initial-branch=${mainBranch}`, quarantine], { cwd: sandboxRoot });
|
|
1988
2040
|
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not initialize quarantine remote').trim());
|
|
1989
2041
|
result = spawnSync('git', ['--git-dir', quarantine, 'bundle', 'unbundle', seedBundle], { encoding: 'utf8', timeout: 30000 });
|
|
1990
2042
|
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not seed quarantine remote').trim());
|
|
1991
2043
|
for (const [repo, ref, oid] of [
|
|
1992
2044
|
[gitDir, `refs/heads/${branch}`, baseOid],
|
|
1993
|
-
[gitDir,
|
|
1994
|
-
[quarantine,
|
|
2045
|
+
[gitDir, `refs/remotes/${baseRef}`, baseOid],
|
|
2046
|
+
[quarantine, `refs/heads/${mainBranch}`, baseOid],
|
|
1995
2047
|
]) {
|
|
1996
2048
|
result = spawnSync('git', ['--git-dir', repo, 'update-ref', ref, oid], { encoding: 'utf8' });
|
|
1997
2049
|
if (result.status !== 0) throw new Error(String(result.stderr || `could not seed ${ref}`).trim());
|
|
@@ -2027,7 +2079,7 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
2027
2079
|
owner: engine || 'one-lap',
|
|
2028
2080
|
task: String(taskId || ''),
|
|
2029
2081
|
branch,
|
|
2030
|
-
base:
|
|
2082
|
+
base: baseRef,
|
|
2031
2083
|
sealed_review_sandbox: true,
|
|
2032
2084
|
created_at: new Date().toISOString(),
|
|
2033
2085
|
}, null, 2)}\n`, 'utf8');
|
|
@@ -2044,6 +2096,8 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
2044
2096
|
originalUrl,
|
|
2045
2097
|
protectedMaster,
|
|
2046
2098
|
protectedRefs,
|
|
2099
|
+
baseRef,
|
|
2100
|
+
mainBranch,
|
|
2047
2101
|
quarantineRefs: bareRefsDigest(quarantine),
|
|
2048
2102
|
branch,
|
|
2049
2103
|
baseOid,
|
|
@@ -2075,9 +2129,9 @@ function reviewRemoteBoundaryState(worktreePath, boundary) {
|
|
|
2075
2129
|
protected_master: boundary.protectedMaster,
|
|
2076
2130
|
};
|
|
2077
2131
|
}
|
|
2078
|
-
const protectedMaster = remoteMasterOid(boundary.trustedRoot, boundary.originalUrl);
|
|
2132
|
+
const protectedMaster = remoteMasterOid(boundary.trustedRoot, boundary.originalUrl, boundary.mainBranch);
|
|
2079
2133
|
const protectedRefs = remoteRefsDigest(boundary.trustedRoot, boundary.originalUrl);
|
|
2080
|
-
const quarantineMasterResult = spawnSync('git', ['--git-dir', boundary.quarantine, 'rev-parse', '--verify',
|
|
2134
|
+
const quarantineMasterResult = spawnSync('git', ['--git-dir', boundary.quarantine, 'rev-parse', '--verify', `refs/heads/${boundary.mainBranch}^{commit}`], { encoding: 'utf8' });
|
|
2081
2135
|
const quarantineMaster = quarantineMasterResult.status === 0 ? String(quarantineMasterResult.stdout || '').trim() : '';
|
|
2082
2136
|
const quarantineRefs = bareRefsDigest(boundary.quarantine);
|
|
2083
2137
|
if (!protectedMaster || !protectedRefs || protectedMaster !== boundary.protectedMaster || protectedRefs !== boundary.protectedRefs) {
|
|
@@ -2330,7 +2384,7 @@ function importReviewCommit(boundary, { cli, taskId, engine, expectedCommit = ''
|
|
|
2330
2384
|
const allowedRefs = new Map([
|
|
2331
2385
|
[`refs/heads/${boundary.branch}`, headOid],
|
|
2332
2386
|
['refs/remotes/origin/HEAD', boundary.baseOid],
|
|
2333
|
-
[
|
|
2387
|
+
[`refs/remotes/${boundary.baseRef}`, boundary.baseOid],
|
|
2334
2388
|
]);
|
|
2335
2389
|
const refLines = String(refs.stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
2336
2390
|
const seenRefs = new Set();
|
|
@@ -2339,7 +2393,7 @@ function importReviewCommit(boundary, { cli, taskId, engine, expectedCommit = ''
|
|
|
2339
2393
|
if (!allowedRefs.has(ref) || allowedRefs.get(ref) !== oid) return fail('sandbox_metadata_changed', `the worker created an unexpected Git ref (${ref || 'unknown'})`);
|
|
2340
2394
|
seenRefs.add(ref);
|
|
2341
2395
|
}
|
|
2342
|
-
for (const ref of [`refs/heads/${boundary.branch}`,
|
|
2396
|
+
for (const ref of [`refs/heads/${boundary.branch}`, `refs/remotes/${boundary.baseRef}`]) {
|
|
2343
2397
|
if (!seenRefs.has(ref)) return fail('sandbox_metadata_changed', `the sealed sandbox is missing a required Git ref (${ref})`);
|
|
2344
2398
|
}
|
|
2345
2399
|
|
|
@@ -2415,7 +2469,7 @@ function disposeReviewSandbox(boundary) {
|
|
|
2415
2469
|
fs.rmSync(boundary.sandboxRoot, { recursive: true, force: true });
|
|
2416
2470
|
}
|
|
2417
2471
|
|
|
2418
|
-
function inspectReviewChange(worktreePath, baseRef =
|
|
2472
|
+
function inspectReviewChange(worktreePath, baseRef = resolveDispatchLandTarget(worktreePath)) {
|
|
2419
2473
|
const run = (args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8', env: trustedGitEnvironment() });
|
|
2420
2474
|
const base = run(['rev-parse', '--verify', `${baseRef}^{commit}`]);
|
|
2421
2475
|
const head = run(['rev-parse', '--verify', 'HEAD^{commit}']);
|
|
@@ -2533,6 +2587,7 @@ async function validateWithAttempts({
|
|
|
2533
2587
|
task,
|
|
2534
2588
|
worktreePath,
|
|
2535
2589
|
executorEngine,
|
|
2590
|
+
baseRef = resolveDispatchLandTarget(worktreePath),
|
|
2536
2591
|
verifierCommand,
|
|
2537
2592
|
validatorEngines,
|
|
2538
2593
|
validatorDispatcher = null,
|
|
@@ -2581,7 +2636,7 @@ async function validateWithAttempts({
|
|
|
2581
2636
|
|
|
2582
2637
|
// One prep pass per prep job for this task: a backup reviewer whose line
|
|
2583
2638
|
// asks for the same prep reuses the brief the first reviewer's prep wrote.
|
|
2584
|
-
const reviewPrompt = buildOneLapValidatorPrompt(task, { verifierCommand, executorEngine });
|
|
2639
|
+
const reviewPrompt = buildOneLapValidatorPrompt(task, { verifierCommand, executorEngine, baseRef });
|
|
2585
2640
|
const prepPasses = new Map();
|
|
2586
2641
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
2587
2642
|
const validatorEngine = candidates[index];
|
|
@@ -2899,8 +2954,8 @@ function defaultDispatchStartCommit({ worktreePath, git = null } = {}) {
|
|
|
2899
2954
|
return String(head.stdout || '').trim();
|
|
2900
2955
|
}
|
|
2901
2956
|
|
|
2902
|
-
function defaultSelfLandCheck({ worktreePath, targetRef =
|
|
2903
|
-
const run = git || (
|
|
2957
|
+
function defaultSelfLandCheck({ worktreePath, targetRef = resolveDispatchLandTarget(worktreePath), startCommit = '', entry = null, git = null } = {}) {
|
|
2958
|
+
const run = git || boundedGit(worktreePath);
|
|
2904
2959
|
const recordedStart = String(startCommit || (entry && entry.startCommit) || '').trim();
|
|
2905
2960
|
if (!recordedStart) {
|
|
2906
2961
|
return { ok: false, stage: 'self_land_check', reason: 'unknown_start_commit', target: targetRef, detail: 'dispatch start commit was not recorded' };
|
|
@@ -2954,7 +3009,7 @@ function defaultSelfLandCheck({ worktreePath, targetRef = DISPATCH_SELF_LAND_TAR
|
|
|
2954
3009
|
const branch = String(targetRef || '').startsWith('origin/') ? String(targetRef).slice('origin/'.length) : '';
|
|
2955
3010
|
const fetch = run(branch ? ['fetch', 'origin', `${branch}:refs/remotes/origin/${branch}`] : ['fetch', 'origin']);
|
|
2956
3011
|
if (fetch.status !== 0) {
|
|
2957
|
-
return { ok: false, stage: 'self_land_check', target: targetRef, start_commit: recordedStart, head, detail:
|
|
3012
|
+
return { ok: false, stage: 'self_land_check', target: targetRef, start_commit: recordedStart, head, detail: gitFailureDetail(fetch) };
|
|
2958
3013
|
}
|
|
2959
3014
|
const ancestor = run(['merge-base', '--is-ancestor', 'HEAD', targetRef]);
|
|
2960
3015
|
if (ancestor.status === 0) return { ok: true, stage: 'self_landed', target: targetRef, start_commit: recordedStart, head };
|
|
@@ -2988,13 +3043,14 @@ async function runFleetFlight({
|
|
|
2988
3043
|
dispatcher = null,
|
|
2989
3044
|
lander = null,
|
|
2990
3045
|
rebase = null,
|
|
2991
|
-
checkoutBase =
|
|
3046
|
+
checkoutBase = null,
|
|
2992
3047
|
guardCliLink = guardGlobalCliLink,
|
|
2993
3048
|
scoutAsk = null,
|
|
2994
3049
|
clock = Date.now,
|
|
2995
3050
|
prepAsk = undefined,
|
|
2996
3051
|
} = {}) {
|
|
2997
3052
|
const cli = ownCli || defaultOwnCli(root);
|
|
3053
|
+
const landTarget = resolveDispatchLandTarget(root, checkoutBase);
|
|
2998
3054
|
// Staff first, rank second: every task staffFlight returns already cleared
|
|
2999
3055
|
// the safe-lane filter (not a decision row, no denied tags), so the rank
|
|
3000
3056
|
// that pairs engines to those tasks is low stakes and the stretch zone
|
|
@@ -3059,13 +3115,13 @@ async function runFleetFlight({
|
|
|
3059
3115
|
}));
|
|
3060
3116
|
const restaffState = { used: false };
|
|
3061
3117
|
|
|
3062
|
-
// Cut every build worktree from
|
|
3118
|
+
// Cut every build worktree from the protected mainline by default, not the launcher's
|
|
3063
3119
|
// HEAD. A flight launched from a long-lived feature-branch checkout would
|
|
3064
3120
|
// otherwise stack all that branch's commits onto the build branch, so
|
|
3065
3121
|
// rebase-before-ship replays them onto master and pauses at rebase_conflict
|
|
3066
3122
|
// (three backend pauses on members.py, 2026-07-05). --base overrides only
|
|
3067
3123
|
// when the operator explicitly wants launcher-HEAD.
|
|
3068
|
-
const startBaseArgs =
|
|
3124
|
+
const startBaseArgs = ['--base', landTarget];
|
|
3069
3125
|
const prepared = [];
|
|
3070
3126
|
for (const entry of staffed) {
|
|
3071
3127
|
const claimed = cli(['task', 'claim', String(entry.task.display_id), '--as', `fleet-${entry.engine}`]);
|
|
@@ -3111,10 +3167,10 @@ async function runFleetFlight({
|
|
|
3111
3167
|
// Land serially: rebase-before-ship, conflict pauses (never auto-resolve).
|
|
3112
3168
|
const rebaseArrival = rebase || landArrival;
|
|
3113
3169
|
const land = lander || (({ entry }) => {
|
|
3114
|
-
const rebased = rebaseArrival({ worktreePath: entry.worktreePath });
|
|
3170
|
+
const rebased = rebaseArrival({ worktreePath: entry.worktreePath, targetRef: landTarget });
|
|
3115
3171
|
if (!rebased.ok) return rebased;
|
|
3116
3172
|
const check = focusedCheck(entry.task) || 'git log -1 --oneline';
|
|
3117
|
-
const shipped = shipWithRetry(cli, fleetShipArgs(entry, check), entry.worktreePath);
|
|
3173
|
+
const shipped = shipWithRetry(cli, fleetShipArgs(entry, check, landTarget), entry.worktreePath);
|
|
3118
3174
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
3119
3175
|
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped) };
|
|
3120
3176
|
}
|
|
@@ -3124,7 +3180,7 @@ async function runFleetFlight({
|
|
|
3124
3180
|
for (const { entry, result } of results) {
|
|
3125
3181
|
const activeEngine = result.engine || entry.engine;
|
|
3126
3182
|
const landingEntry = { ...entry, engine: activeEngine };
|
|
3127
|
-
if (result.restaffed) log(` restaffed ${entry.task.display_id}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
|
|
3183
|
+
if (result.restaffed) log(` restaffed ${entry.task.display_id}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.detail || result.restaffed.reason})`);
|
|
3128
3184
|
if (result.handover) log(` handed over ${entry.task.display_id}: ${result.handover_reason}`);
|
|
3129
3185
|
const buildFailure = detectDeadEngineDispatch(result);
|
|
3130
3186
|
if (buildFailure) {
|
|
@@ -3133,14 +3189,14 @@ async function runFleetFlight({
|
|
|
3133
3189
|
engine: activeEngine,
|
|
3134
3190
|
stage: 'build',
|
|
3135
3191
|
reason: buildFailure.reason,
|
|
3136
|
-
detail: String(result.stderr || buildFailure.reason).slice(0, 200),
|
|
3192
|
+
detail: buildFailure.detail || String(result.stderr || buildFailure.reason).slice(0, 200),
|
|
3137
3193
|
};
|
|
3138
3194
|
if (result.restaffed) paused.restaffed = result.restaffed;
|
|
3139
3195
|
if (result.handover) paused.handover = result.handover;
|
|
3140
3196
|
if (result.deadEngine) paused.deadEngine = result.deadEngine;
|
|
3141
3197
|
flight.paused.push(paused);
|
|
3142
3198
|
stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.task.display_id}`);
|
|
3143
|
-
log(` ${activeEngine.padEnd(8)} ✗ build failed ${entry.task.display_id}, worktree kept for takeover`);
|
|
3199
|
+
log(` ${activeEngine.padEnd(8)} ✗ build failed ${entry.task.display_id}: ${plainDispatchFailureCause(result, buildFailure)}, worktree kept for takeover`);
|
|
3144
3200
|
continue;
|
|
3145
3201
|
}
|
|
3146
3202
|
log(` ${activeEngine.padEnd(8)} landing ${entry.task.display_id}...`);
|
|
@@ -3187,7 +3243,7 @@ async function runFleetFlight({
|
|
|
3187
3243
|
paused: flight.paused,
|
|
3188
3244
|
worktreeByTask: new Map(prepared.map((entry) => [String(entry.task.display_id), entry.worktreePath])),
|
|
3189
3245
|
actorByTask: new Map(staffed.map((entry) => [String(entry.task.display_id), `fleet-${entry.engine}`])),
|
|
3190
|
-
baseRef:
|
|
3246
|
+
baseRef: landTarget,
|
|
3191
3247
|
log,
|
|
3192
3248
|
});
|
|
3193
3249
|
|
|
@@ -3222,7 +3278,7 @@ function releaseUnstartedDispatchClaim(cli, { taskId, actor, detail }) {
|
|
|
3222
3278
|
// paused task whose worktree holds no commits past base and nothing but
|
|
3223
3279
|
// conductor litter gets its claim released; a paused task WITH work keeps the
|
|
3224
3280
|
// claim so takeover stays attributed.
|
|
3225
|
-
function releasePausedClaimsWithoutWork(cli, { paused, worktreeByTask, actorByTask, baseRef = '
|
|
3281
|
+
function releasePausedClaimsWithoutWork(cli, { paused, worktreeByTask, actorByTask, baseRef = '', log = () => {} }) {
|
|
3226
3282
|
for (const row of paused || []) {
|
|
3227
3283
|
if (row.stage === 'claim' || row.stage === 'task_lookup' || row.landed === true) continue; // no claim, or work already landed
|
|
3228
3284
|
if (row.claim_released !== undefined) continue;
|
|
@@ -3231,7 +3287,9 @@ function releasePausedClaimsWithoutWork(cli, { paused, worktreeByTask, actorByTa
|
|
|
3231
3287
|
if (!taskId || !actor) continue;
|
|
3232
3288
|
const worktreePath = row.worktree || worktreeByTask.get(taskId) || '';
|
|
3233
3289
|
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
3234
|
-
const
|
|
3290
|
+
const resolvedBase = baseRef || defaultMainlineBase(worktreePath);
|
|
3291
|
+
if (!baseRef && resolvedBase === 'HEAD') continue; // no known start point: preserve the claim
|
|
3292
|
+
const ahead = spawnSync('git', ['-C', worktreePath, 'rev-list', '--count', `${resolvedBase}..HEAD`], { encoding: 'utf8' });
|
|
3235
3293
|
if (ahead.status !== 0 || parseInt(String(ahead.stdout).trim(), 10) > 0) continue;
|
|
3236
3294
|
// -uall lists untracked files one by one; the default collapses a fully
|
|
3237
3295
|
// untracked .atris/ into "?? .atris/", which the allowlist cannot judge.
|
|
@@ -3319,7 +3377,13 @@ async function runDispatchFlight({
|
|
|
3319
3377
|
const cli = ownCli || defaultOwnCli(root);
|
|
3320
3378
|
// Cut worktrees from, and check self-landing against, the workspace's real
|
|
3321
3379
|
// protected branch, not a hard-coded origin/master (CLI-1298).
|
|
3322
|
-
const
|
|
3380
|
+
const localBase = reviewOnly ? '' : localDispatchBase(root);
|
|
3381
|
+
const localOnly = Boolean(localBase);
|
|
3382
|
+
const proofOnly = reviewOnly || localOnly;
|
|
3383
|
+
const landTarget = localOnly ? (checkoutBase || localBase) : resolveDispatchLandTarget(root, checkoutBase);
|
|
3384
|
+
const localBranch = localOnly
|
|
3385
|
+
? String(spawnSync('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd: root, encoding: 'utf8' }).stdout || '').trim()
|
|
3386
|
+
: '';
|
|
3323
3387
|
const verify = verifier || (trustedVerifier ? defaultTrustedVerifyRunner : defaultVerifyRunner);
|
|
3324
3388
|
const inspectChange = changeInspector || inspectReviewChange;
|
|
3325
3389
|
const readStartCommit = startCommitReader || defaultDispatchStartCommit;
|
|
@@ -3339,7 +3403,8 @@ async function runDispatchFlight({
|
|
|
3339
3403
|
paused: [],
|
|
3340
3404
|
};
|
|
3341
3405
|
if (yolo) flight.yolo = true;
|
|
3342
|
-
if (
|
|
3406
|
+
if (proofOnly) flight.review_only = true;
|
|
3407
|
+
if (localOnly) flight.local_only = true;
|
|
3343
3408
|
flight.actor = taskActor;
|
|
3344
3409
|
if (receiptContext && typeof receiptContext === 'object') flight.context = receiptContext;
|
|
3345
3410
|
const requiresIndependentValidator = reviewOnly && flight.context && flight.context.source === 'one_lap';
|
|
@@ -3371,7 +3436,7 @@ async function runDispatchFlight({
|
|
|
3371
3436
|
let landingWorktreePath = '';
|
|
3372
3437
|
let remoteBoundary = null;
|
|
3373
3438
|
if (enforceRemoteBoundary) {
|
|
3374
|
-
remoteBoundary = prepareReviewSandbox({ root, taskId, engine });
|
|
3439
|
+
remoteBoundary = prepareReviewSandbox({ root, taskId, engine, baseRef: landTarget });
|
|
3375
3440
|
} else {
|
|
3376
3441
|
const started = cli(['worktree', 'start', '--agent', engine, '--task', `dispatch-${taskId.toLowerCase()}`, ...startBaseArgs]);
|
|
3377
3442
|
const wt = (started.stdout.match(/next: cd (.+)/) || [])[1];
|
|
@@ -3405,13 +3470,13 @@ async function runDispatchFlight({
|
|
|
3405
3470
|
}
|
|
3406
3471
|
const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
|
|
3407
3472
|
const startCommit = yolo ? readStartCommit({ worktreePath }) : '';
|
|
3408
|
-
const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
|
|
3409
|
-
const safetyPrompt =
|
|
3473
|
+
const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo: yolo && !localOnly });
|
|
3474
|
+
const safetyPrompt = proofOnly
|
|
3410
3475
|
? `${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.`
|
|
3411
3476
|
: basePrompt;
|
|
3412
3477
|
const trustedPrompt = trustedVerifier
|
|
3413
3478
|
? `${safetyPrompt}\n\nTrusted verifier (run it bare before reporting done): ${trustedVerifier}`
|
|
3414
|
-
: promptOverride;
|
|
3479
|
+
: (localOnly ? safetyPrompt : promptOverride);
|
|
3415
3480
|
prepared.push({
|
|
3416
3481
|
task,
|
|
3417
3482
|
taskId,
|
|
@@ -3518,16 +3583,16 @@ async function runDispatchFlight({
|
|
|
3518
3583
|
const rebaseArrival = rebase || landArrival;
|
|
3519
3584
|
const checkSelfLand = selfLandCheck || defaultSelfLandCheck;
|
|
3520
3585
|
const land = lander || (({ entry }) => {
|
|
3521
|
-
const rebased = enforceRemoteBoundary
|
|
3586
|
+
const rebased = enforceRemoteBoundary || localOnly
|
|
3522
3587
|
? { ok: true, stage: 'frozen_base' }
|
|
3523
|
-
: rebaseArrival({ worktreePath: entry.worktreePath });
|
|
3588
|
+
: rebaseArrival({ worktreePath: entry.worktreePath, targetRef: landTarget });
|
|
3524
3589
|
if (!rebased.ok) return rebased;
|
|
3525
3590
|
if (enforceRemoteBoundary) {
|
|
3526
3591
|
const candidate = reviewCandidateSnapshot(entry.remoteBoundary, entry.candidate);
|
|
3527
3592
|
if (!candidate.ok) return candidate;
|
|
3528
3593
|
}
|
|
3529
|
-
const change =
|
|
3530
|
-
if (
|
|
3594
|
+
const change = proofOnly ? inspectChange(entry.worktreePath, landTarget) : null;
|
|
3595
|
+
if (proofOnly && (!change || change.has_change !== true)) {
|
|
3531
3596
|
return {
|
|
3532
3597
|
ok: false,
|
|
3533
3598
|
stage: 'no_change',
|
|
@@ -3535,6 +3600,9 @@ async function runDispatchFlight({
|
|
|
3535
3600
|
change: change || null,
|
|
3536
3601
|
};
|
|
3537
3602
|
}
|
|
3603
|
+
if (localOnly && (!change.commit || change.dirty)) {
|
|
3604
|
+
return { ok: false, stage: 'uncommitted_change', detail: 'commit the local change before merging it', change };
|
|
3605
|
+
}
|
|
3538
3606
|
if (enforceRemoteBoundary && (change.head !== entry.candidate.commit || change.dirty)) {
|
|
3539
3607
|
return {
|
|
3540
3608
|
ok: false,
|
|
@@ -3569,7 +3637,7 @@ async function runDispatchFlight({
|
|
|
3569
3637
|
const candidate = reviewCandidateSnapshot(entry.remoteBoundary, entry.candidate);
|
|
3570
3638
|
if (!candidate.ok) return { ...candidate, check, verifier_result: verifierResult };
|
|
3571
3639
|
}
|
|
3572
|
-
if (
|
|
3640
|
+
if (proofOnly) {
|
|
3573
3641
|
return {
|
|
3574
3642
|
ok: true,
|
|
3575
3643
|
stage: 'verified_for_review',
|
|
@@ -3579,7 +3647,7 @@ async function runDispatchFlight({
|
|
|
3579
3647
|
change,
|
|
3580
3648
|
};
|
|
3581
3649
|
}
|
|
3582
|
-
const shipped = shipWithRetry(cli, fleetShipArgs({ task: entry.task, engine: entry.engine || engine }, check), entry.worktreePath);
|
|
3650
|
+
const shipped = shipWithRetry(cli, fleetShipArgs({ task: entry.task, engine: entry.engine || engine }, check, landTarget), entry.worktreePath);
|
|
3583
3651
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
3584
3652
|
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped) };
|
|
3585
3653
|
}
|
|
@@ -3596,7 +3664,7 @@ async function runDispatchFlight({
|
|
|
3596
3664
|
const activeEngine = result.engine || entry.engine || engine;
|
|
3597
3665
|
const readyActor = explicitActor || `fleet-${activeEngine}`;
|
|
3598
3666
|
const landingEntry = { ...entry, engine: activeEngine, candidate };
|
|
3599
|
-
if (result.restaffed) log(` restaffed ${entry.taskId}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
|
|
3667
|
+
if (result.restaffed) log(` restaffed ${entry.taskId}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.detail || result.restaffed.reason})`);
|
|
3600
3668
|
if (result.handover) log(` handed over ${entry.taskId}: ${result.handover_reason}`);
|
|
3601
3669
|
const buildFailure = detectDeadEngineDispatch(result);
|
|
3602
3670
|
if (buildFailure) {
|
|
@@ -3605,7 +3673,7 @@ async function runDispatchFlight({
|
|
|
3605
3673
|
engine: activeEngine,
|
|
3606
3674
|
stage: 'build',
|
|
3607
3675
|
reason: buildFailure.reason,
|
|
3608
|
-
detail: String(result.stderr || buildFailure.reason).slice(0, 300),
|
|
3676
|
+
detail: buildFailure.detail || String(result.stderr || buildFailure.reason).slice(0, 300),
|
|
3609
3677
|
worktree: entry.worktreePath,
|
|
3610
3678
|
};
|
|
3611
3679
|
if (result.restaffed) paused.restaffed = result.restaffed;
|
|
@@ -3613,7 +3681,7 @@ async function runDispatchFlight({
|
|
|
3613
3681
|
if (result.deadEngine) paused.deadEngine = result.deadEngine;
|
|
3614
3682
|
flight.paused.push(paused);
|
|
3615
3683
|
stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.taskId}`);
|
|
3616
|
-
log(` ✗ build failed ${entry.taskId}, worktree kept for takeover`);
|
|
3684
|
+
log(` ✗ build failed ${entry.taskId}: ${plainDispatchFailureCause(result, buildFailure)}, worktree kept for takeover`);
|
|
3617
3685
|
continue;
|
|
3618
3686
|
}
|
|
3619
3687
|
if (enforceRemoteBoundary && (!candidate || candidate.ok !== true)) {
|
|
@@ -3646,7 +3714,7 @@ async function runDispatchFlight({
|
|
|
3646
3714
|
continue;
|
|
3647
3715
|
}
|
|
3648
3716
|
}
|
|
3649
|
-
if (yolo) {
|
|
3717
|
+
if (yolo && !localOnly) {
|
|
3650
3718
|
log(` checking self-land ${entry.taskId}...`);
|
|
3651
3719
|
const selfLanded = checkSelfLand({
|
|
3652
3720
|
entry,
|
|
@@ -3690,7 +3758,7 @@ async function runDispatchFlight({
|
|
|
3690
3758
|
}
|
|
3691
3759
|
continue;
|
|
3692
3760
|
}
|
|
3693
|
-
log(` ${
|
|
3761
|
+
log(` ${proofOnly ? 'checking' : 'landing'} ${entry.taskId}...`);
|
|
3694
3762
|
const landed = land({ entry: landingEntry, result });
|
|
3695
3763
|
stampDispatchResultVerification(
|
|
3696
3764
|
flight,
|
|
@@ -3702,7 +3770,7 @@ async function runDispatchFlight({
|
|
|
3702
3770
|
if (landed.ok) {
|
|
3703
3771
|
const verifyTail = String(landed.verifyOutput || '').trim().slice(-1200).replace(/\n/g, ' ');
|
|
3704
3772
|
const restaffProof = result.restaffed ? ` Restaffed from ${result.restaffed.from} to ${result.restaffed.to} (${result.restaffed.reason}).` : '';
|
|
3705
|
-
if (
|
|
3773
|
+
if (proofOnly) {
|
|
3706
3774
|
let finalMaster = entry.remoteMasterBefore;
|
|
3707
3775
|
const boundaryBeforeReview = enforceRemoteBoundary
|
|
3708
3776
|
? reviewRemoteBoundaryState(entry.worktreePath, entry.remoteBoundary)
|
|
@@ -3746,6 +3814,7 @@ async function runDispatchFlight({
|
|
|
3746
3814
|
task: entry.task,
|
|
3747
3815
|
worktreePath: entry.worktreePath,
|
|
3748
3816
|
executorEngine: activeEngine,
|
|
3817
|
+
baseRef: entry.remoteBoundary ? entry.remoteBoundary.baseOid : landTarget,
|
|
3749
3818
|
verifierCommand: landed.check,
|
|
3750
3819
|
validatorEngines,
|
|
3751
3820
|
validatorDispatcher,
|
|
@@ -3893,11 +3962,13 @@ async function runDispatchFlight({
|
|
|
3893
3962
|
finalMaster = boundaryAfterImport.protected_master || finalMaster;
|
|
3894
3963
|
disposeReviewSandbox(entry.remoteBoundary);
|
|
3895
3964
|
}
|
|
3896
|
-
const shipArgs = fleetShipArgs({ task: entry.task, engine: activeEngine }, landed.check);
|
|
3965
|
+
const shipArgs = fleetShipArgs({ task: entry.task, engine: activeEngine }, landed.check, landTarget);
|
|
3897
3966
|
const shellQuote = (value) => /^[A-Za-z0-9_./:-]+$/.test(String(value))
|
|
3898
3967
|
? String(value)
|
|
3899
3968
|
: `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
3900
|
-
const nextCommand =
|
|
3969
|
+
const nextCommand = localOnly
|
|
3970
|
+
? `cd ${shellQuote(root)}${localBranch ? ` && git switch ${shellQuote(localBranch)}` : ''} && git merge --ff-only ${shellQuote(reviewChange.commit)}`
|
|
3971
|
+
: `cd ${shellQuote(reviewWorktreePath)} && atris ${shipArgs.map(shellQuote).join(' ')}`;
|
|
3901
3972
|
const readyRow = {
|
|
3902
3973
|
task: entry.taskId,
|
|
3903
3974
|
engine: activeEngine,
|
|
@@ -3917,7 +3988,7 @@ async function runDispatchFlight({
|
|
|
3917
3988
|
if (result.restaffed) readyRow.restaffed = result.restaffed;
|
|
3918
3989
|
|
|
3919
3990
|
flight.ready.push(readyRow);
|
|
3920
|
-
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
3991
|
+
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly: proofOnly, enforceRemoteBoundary });
|
|
3921
3992
|
const verificationCommit = String(reviewChange.source_commit || reviewChange.commit || reviewChange.head || '').trim();
|
|
3922
3993
|
const verificationCitation = verificationCommit
|
|
3923
3994
|
? ` Verification snapshot: commit ${verificationCommit}${reviewChange.dirty ? ' with a worktree diff' : ''}; the declared verifier passed (exit 0).`
|
|
@@ -3946,7 +4017,7 @@ async function runDispatchFlight({
|
|
|
3946
4017
|
worktree: reviewWorktreePath,
|
|
3947
4018
|
change: reviewChange,
|
|
3948
4019
|
});
|
|
3949
|
-
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
4020
|
+
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly: proofOnly, enforceRemoteBoundary });
|
|
3950
4021
|
stampDispatchBrief(root, result.brief_id, 'partial', `verified ${entry.taskId}, but task ready failed`);
|
|
3951
4022
|
log(` paused ${entry.taskId} at task_ready - worktree kept`);
|
|
3952
4023
|
continue;
|
|
@@ -3954,6 +4025,10 @@ async function runDispatchFlight({
|
|
|
3954
4025
|
readyRow.review_recorded = true;
|
|
3955
4026
|
stampDispatchBrief(root, result.brief_id, 'pass', `verified ${entry.taskId} for Review`);
|
|
3956
4027
|
log(` proof ready ${entry.taskId}`);
|
|
4028
|
+
if (localOnly) {
|
|
4029
|
+
log(' the verified change is ready on a local branch. review it, then merge it locally:');
|
|
4030
|
+
log(` next: ${nextCommand}`);
|
|
4031
|
+
}
|
|
3957
4032
|
continue;
|
|
3958
4033
|
}
|
|
3959
4034
|
const landedRow = { task: entry.taskId, engine: activeEngine, check: landed.check };
|
|
@@ -3968,7 +4043,7 @@ async function runDispatchFlight({
|
|
|
3968
4043
|
'--checked', `${landed.check} passed before landing`,
|
|
3969
4044
|
'--tested', 'The requested behavior passed its declared verifier.',
|
|
3970
4045
|
'--as', readyActor,
|
|
3971
|
-
], { flight, landedRow, worktree: entry.worktreePath, save: () => writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary }) });
|
|
4046
|
+
], { flight, landedRow, worktree: entry.worktreePath, save: () => writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly: proofOnly, enforceRemoteBoundary }) });
|
|
3972
4047
|
if (!reviewRecorded) {
|
|
3973
4048
|
stampDispatchBrief(root, result.brief_id, 'partial', `landed ${entry.taskId}, but task ready failed`);
|
|
3974
4049
|
log(' the change landed, but saving its proof failed. repair the task record before starting more work.');
|
|
@@ -4004,7 +4079,7 @@ async function runDispatchFlight({
|
|
|
4004
4079
|
});
|
|
4005
4080
|
flight.status = flight.paused.length ? 'failed' : 'completed';
|
|
4006
4081
|
flight.finished_at = new Date().toISOString();
|
|
4007
|
-
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
4082
|
+
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly: proofOnly, enforceRemoteBoundary });
|
|
4008
4083
|
log('');
|
|
4009
4084
|
if (engine === 'fable' && flight.paused.length) {
|
|
4010
4085
|
const paused = flight.paused[0];
|
|
@@ -4012,7 +4087,7 @@ async function runDispatchFlight({
|
|
|
4012
4087
|
const cause = plainDispatchFailureCause(failed && failed.result, paused);
|
|
4013
4088
|
log(` fable handoff failed: ${cause}. receipt: ${path.relative(root, receiptPath)}`);
|
|
4014
4089
|
}
|
|
4015
|
-
const completedLabel =
|
|
4090
|
+
const completedLabel = proofOnly ? `${flight.ready.length} proof ready` : `${flight.landed.length} landed`;
|
|
4016
4091
|
log(` dispatch over: ${completedLabel}, ${flight.paused.length} paused - receipt: ${path.relative(root, flight.receipt)}`);
|
|
4017
4092
|
log('');
|
|
4018
4093
|
return flight;
|