atris 3.68.1 → 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.
Files changed (41) hide show
  1. package/atris/skills/engines/SKILL.md +1 -1
  2. package/atris/team/customer-lead/MEMBER.md +1 -1
  3. package/atris/team/executor/MEMBER.md +19 -18
  4. package/atris/team/executor/skills/isolated-slice-builder/SKILL.md +21 -0
  5. package/atris/team/navigator/MEMBER.md +3 -1
  6. package/atris/team/navigator/skills/loophole-proof-planner/SKILL.md +20 -0
  7. package/atris/team/researcher/MEMBER.md +2 -1
  8. package/atris/team/researcher/skills/grounded-evidence-brief/SKILL.md +19 -0
  9. package/atris/team/validator/MEMBER.md +2 -7
  10. package/atris/team/validator/skills/cross-model-referee/SKILL.md +20 -0
  11. package/bin/atris.js +40 -1
  12. package/commands/autoland.js +2 -1
  13. package/commands/done.js +374 -0
  14. package/commands/engine.js +2 -2
  15. package/commands/goal.js +61 -2
  16. package/commands/land.js +34 -7
  17. package/commands/member.js +1542 -41
  18. package/commands/mission.js +506 -18
  19. package/commands/task.js +26 -3
  20. package/commands/workflow.js +1 -1
  21. package/commands/worktree.js +144 -34
  22. package/lib/auto-accept-certified.js +7 -6
  23. package/lib/codex-flight.js +6 -12
  24. package/lib/conductor-artifacts.js +3 -1
  25. package/lib/default-runner.js +42 -7
  26. package/lib/engine-ask.js +8 -1
  27. package/lib/engine-registry.js +17 -0
  28. package/lib/falsifier-probe.js +6 -0
  29. package/lib/first-minute.js +257 -6
  30. package/lib/fleet.js +163 -88
  31. package/lib/known-commands.js +1 -1
  32. package/lib/member-alive.js +1 -1
  33. package/lib/member-approval.js +153 -0
  34. package/lib/mission-task-gate.js +170 -0
  35. package/lib/one-lap-validator.js +2 -1
  36. package/lib/roster-runs.js +28 -0
  37. package/lib/runner-command.js +32 -8
  38. package/lib/task-db.js +25 -1
  39. package/package.json +9 -2
  40. package/scripts/det/ytnotes +49 -10
  41. package/utils/backend-root.js +4 -0
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 = 'origin/master';
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 (remote HEAD,
180
- // then origin/main), falling back to the historical origin/master when nothing
181
- // else can be determined so single-branch repos and test roots behave as before.
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
- try {
185
- const mainline = defaultMainlineBase(root);
186
- if (mainline && mainline !== 'HEAD') return mainline;
187
- } catch {}
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
- if (terminal.reason === 'ok') return null;
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 master. Without the explicit --target, ship
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', 'origin/master',
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
- function landArrival({ worktreePath, git = null }) {
1486
- const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
1487
- const fetch = run(['fetch', 'origin']);
1488
- if (fetch.status !== 0) return { ok: false, stage: 'fetch', detail: String(fetch.stderr || '').trim() };
1489
- const rebase = run(['rebase', 'origin/master']);
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
- const dirty = String(run(['status', '--porcelain']).stdout || '')
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 = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
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, 'refs/heads/master'], {
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
- const fetched = runConductorGit(['fetch', 'origin', 'master']);
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', 'origin/master^{commit}']);
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: 'review-only dispatch requires a current protected origin/master' };
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: 'review-only origin/master is not at the protected remote snapshot' };
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, 'origin/master']);
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', '--initial-branch=master', gitDir], { cwd: sandboxRoot });
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', '--initial-branch=master', quarantine], { cwd: sandboxRoot });
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, 'refs/remotes/origin/master', baseOid],
1994
- [quarantine, 'refs/heads/master', baseOid],
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: 'origin/master',
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', 'refs/heads/master^{commit}'], { encoding: 'utf8' });
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
- ['refs/remotes/origin/master', boundary.baseOid],
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}`, 'refs/remotes/origin/master']) {
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 = 'origin/master') {
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 = DISPATCH_SELF_LAND_TARGET, startCommit = '', entry = null, git = null } = {}) {
2903
- const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
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: String(fetch.stderr || fetch.stdout || '').trim() };
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 = 'origin/master',
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 origin/master by default, not the launcher's
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 = checkoutBase ? ['--base', checkoutBase] : [];
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: checkoutBase || 'origin/master',
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 = 'origin/master', log = () => {} }) {
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 ahead = spawnSync('git', ['-C', worktreePath, 'rev-list', '--count', `${baseRef}..HEAD`], { encoding: 'utf8' });
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 landTarget = resolveDispatchLandTarget(root, checkoutBase);
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 (reviewOnly) flight.review_only = true;
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 = reviewOnly
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 = reviewOnly ? inspectChange(entry.worktreePath, landTarget) : null;
3530
- if (reviewOnly && (!change || change.has_change !== true)) {
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 (reviewOnly) {
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(` ${reviewOnly ? 'checking' : 'landing'} ${entry.taskId}...`);
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 (reviewOnly) {
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 = `cd ${shellQuote(reviewWorktreePath)} && atris ${shipArgs.map(shellQuote).join(' ')}`;
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 = reviewOnly ? `${flight.ready.length} proof ready` : `${flight.landed.length} landed`;
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;