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
@@ -103,6 +103,8 @@ const {
103
103
  normalizeHumanAsks,
104
104
  openHumanAsks,
105
105
  } = require('../lib/mission-human-asks');
106
+ const { missionLinkedTaskGate } = require('../lib/mission-task-gate');
107
+ const { memberApprovalAsks, runBlockingAsks } = require('../lib/member-approval');
106
108
  const {
107
109
  inspectMissionProtectedDiff,
108
110
  prepareMissionGitGuard,
@@ -730,6 +732,7 @@ const MISSION_RUN_VALUE_FLAGS = [
730
732
  '--stop',
731
733
  '--model',
732
734
  '--engine',
735
+ '--referee-engine',
733
736
  '--budget',
734
737
  '--repo',
735
738
  '--native-goal-status',
@@ -750,6 +753,8 @@ const MISSION_RUN_BOOLEAN_FLAGS = [
750
753
  '--land',
751
754
  '--no-claude',
752
755
  '--no-verify',
756
+ '--referee',
757
+ '--no-referee',
753
758
  '--complete-on-pass',
754
759
  '--no-drain',
755
760
  '--create-next',
@@ -1621,6 +1626,28 @@ function appendJsonLine(file, payload) {
1621
1626
  fs.appendFileSync(file, JSON.stringify(payload) + '\n', 'utf8');
1622
1627
  }
1623
1628
 
1629
+ function resolveMemberWorkspaceRoot(root = process.cwd()) {
1630
+ const resolved = path.resolve(root || process.cwd());
1631
+ const agentWorktreesMarker = `${path.sep}.agent-worktrees${path.sep}`;
1632
+ const agentIdx = resolved.indexOf(agentWorktreesMarker);
1633
+ if (agentIdx > 0) {
1634
+ const parentDir = resolved.slice(0, agentIdx);
1635
+ const after = resolved.slice(agentIdx + agentWorktreesMarker.length);
1636
+ const repoName = after.split(path.sep)[0];
1637
+ if (repoName) {
1638
+ const primaryRoot = path.join(parentDir, repoName);
1639
+ if (fs.existsSync(path.join(primaryRoot, 'atris'))) return primaryRoot;
1640
+ }
1641
+ }
1642
+ const marker = `${path.sep}.atris${path.sep}worktrees${path.sep}`;
1643
+ const idx = resolved.indexOf(marker);
1644
+ if (idx > 0) {
1645
+ const parentRoot = resolved.slice(0, idx);
1646
+ if (fs.existsSync(path.join(parentRoot, 'atris'))) return parentRoot;
1647
+ }
1648
+ return resolved;
1649
+ }
1650
+
1624
1651
  function appendEvent(type, mission, payload = {}, root = process.cwd()) {
1625
1652
  const paths = statePaths(root);
1626
1653
  const event = {
@@ -1632,6 +1659,12 @@ function appendEvent(type, mission, payload = {}, root = process.cwd()) {
1632
1659
  payload,
1633
1660
  };
1634
1661
  appendJsonLine(paths.eventsJsonl, event);
1662
+ const parentRoot = resolveMemberWorkspaceRoot(root);
1663
+ if (type === 'mission_tick' && parentRoot !== path.resolve(root || process.cwd())) {
1664
+ try {
1665
+ appendJsonLine(statePaths(parentRoot).eventsJsonl, event);
1666
+ } catch {}
1667
+ }
1635
1668
  return event;
1636
1669
  }
1637
1670
 
@@ -1662,9 +1695,15 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
1662
1695
 
1663
1696
  function memberDir(owner, root = process.cwd()) {
1664
1697
  if (!owner || !/^[a-zA-Z0-9._-]+$/.test(owner)) return null;
1665
- const dir = path.join(root, 'atris', 'team', owner);
1666
- if (!fs.existsSync(path.join(dir, 'MEMBER.md'))) return null;
1667
- return dir;
1698
+ const parentRoot = resolveMemberWorkspaceRoot(root);
1699
+ const candidates = parentRoot !== path.resolve(root || process.cwd())
1700
+ ? [parentRoot, root]
1701
+ : [root];
1702
+ for (const base of candidates) {
1703
+ const dir = path.join(base, 'atris', 'team', owner);
1704
+ if (fs.existsSync(path.join(dir, 'MEMBER.md'))) return dir;
1705
+ }
1706
+ return null;
1668
1707
  }
1669
1708
 
1670
1709
  function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
@@ -1686,6 +1725,158 @@ function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
1686
1725
  return logPath;
1687
1726
  }
1688
1727
 
1728
+ function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
1729
+ const dir = memberDir(owner, root);
1730
+ if (!dir) return null;
1731
+ const summary = String(entry.summary || entry.objective || '').replace(/\s+/g, ' ').trim();
1732
+ if (!summary) return null;
1733
+ const skillDir = path.join(dir, 'skills', 'auto-learned');
1734
+ fs.mkdirSync(skillDir, { recursive: true });
1735
+ const skillPath = path.join(skillDir, 'SKILL.md');
1736
+ const existingBullets = [];
1737
+ if (fs.existsSync(skillPath)) {
1738
+ try {
1739
+ const raw = fs.readFileSync(skillPath, 'utf8');
1740
+ for (const line of raw.split(/\r?\n/)) {
1741
+ if (line.trim().startsWith('- **')) existingBullets.push(line.trim());
1742
+ }
1743
+ } catch {}
1744
+ }
1745
+ const verifierText = String(entry.verifier || '').replace(/\s+/g, ' ').trim();
1746
+ const nextPushText = String(entry.next_push || '').replace(/\s+/g, ' ').trim();
1747
+ const layerLabel = String(entry.layer || 'capabilities').trim();
1748
+ const parts = [`- **${todayName()} (${layerLabel})**: ${summary.slice(0, 140)}`];
1749
+ if (verifierText) parts.push(`Verify: \`${verifierText.slice(0, 80)}\``);
1750
+ if (nextPushText) parts.push(`Next push: ${nextPushText.slice(0, 120)}`);
1751
+ const newBullet = parts.join(' -> ');
1752
+ const deduped = [
1753
+ newBullet,
1754
+ ...existingBullets.filter((b) => !b.includes(summary.slice(0, 80))),
1755
+ ].slice(0, 5);
1756
+
1757
+ const content = [
1758
+ '---',
1759
+ 'name: auto-learned',
1760
+ `description: Verified patterns crystallized from passing runs for ${owner}. Bounded to 5 newest patterns so context never bloats.`,
1761
+ '---',
1762
+ '',
1763
+ '# Auto-Learned Verified Patterns',
1764
+ '',
1765
+ ...deduped,
1766
+ '',
1767
+ ].join('\n');
1768
+ fs.writeFileSync(skillPath, content, 'utf8');
1769
+ return skillPath;
1770
+ }
1771
+
1772
+ function autoQueueMemberNextPush(owner, nextPush, parentObjective = '', root = process.cwd(), { promote = false } = {}) {
1773
+ const cleanPush = String(nextPush || '').replace(/\s+/g, ' ').trim();
1774
+ if (!cleanPush) return null;
1775
+ const dir = memberDir(owner, root);
1776
+ if (!dir) return null;
1777
+ const goalsPath = path.join(dir, 'goals.json');
1778
+ let store = { schema_version: 1, member: owner, updated_at: stampIso(), goals: [] };
1779
+ if (fs.existsSync(goalsPath)) {
1780
+ try {
1781
+ const parsed = JSON.parse(fs.readFileSync(goalsPath, 'utf8'));
1782
+ if (parsed && Array.isArray(parsed.goals)) store = parsed;
1783
+ } catch {}
1784
+ }
1785
+ const now = stampIso();
1786
+ const activeIdx = store.goals.findIndex((g) => g && g.status === 'active');
1787
+ if (activeIdx >= 0) {
1788
+ const prev = store.goals[activeIdx];
1789
+ store.goals[activeIdx] = {
1790
+ ...prev,
1791
+ ...(promote ? { previous_title: prev.title, title: cleanPush } : {}),
1792
+ next_push: cleanPush,
1793
+ updated_at: now,
1794
+ };
1795
+ } else {
1796
+ store.goals.push({
1797
+ id: `goal_${Date.now().toString(36)}`,
1798
+ member: owner,
1799
+ title: cleanPush,
1800
+ why: parentObjective ? `Compounds after verified win on: ${parentObjective}` : 'Self-proposed next optimization from verified run.',
1801
+ metric: 'verified_slices_shipped',
1802
+ target: '1',
1803
+ horizon: '7d',
1804
+ acceptance: ['Passes test check and cross-model referee'],
1805
+ next_push: cleanPush,
1806
+ status: 'active',
1807
+ created_at: now,
1808
+ updated_at: now,
1809
+ });
1810
+ }
1811
+ store.updated_at = now;
1812
+ fs.writeFileSync(goalsPath, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
1813
+ return goalsPath;
1814
+ }
1815
+
1816
+ const DEFAULT_MEMBER_BATON_RELAY = {
1817
+ researcher: 'navigator',
1818
+ 'customer-lead': 'navigator',
1819
+ navigator: 'executor',
1820
+ architect: 'executor',
1821
+ executor: 'validator',
1822
+ 'auto-improver': 'validator',
1823
+ };
1824
+
1825
+ function recordMemberHandoff(fromMember, toMember, sliceSummary, root = process.cwd(), extra = {}) {
1826
+ const from = String(fromMember || '').trim().toLowerCase();
1827
+ const to = String(toMember || DEFAULT_MEMBER_BATON_RELAY[from] || '').trim().toLowerCase();
1828
+ const cleanSummary = String(sliceSummary || '').replace(/\s+/g, ' ').trim();
1829
+ if (!from || !to || !cleanSummary) return null;
1830
+ const parentRoot = resolveMemberWorkspaceRoot(root);
1831
+ if (!memberDir(to, parentRoot)) return null;
1832
+ const now = stampIso();
1833
+ const handoff = {
1834
+ id: `handoff_${Date.now().toString(36)}`,
1835
+ from,
1836
+ to,
1837
+ summary: cleanSummary.slice(0, 180),
1838
+ status: extra.status || 'open',
1839
+ next_command: `atris member go ${to}`,
1840
+ created_at: now,
1841
+ };
1842
+ try {
1843
+ const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
1844
+ fs.mkdirSync(path.dirname(handoffsFile), { recursive: true });
1845
+ fs.appendFileSync(handoffsFile, `${JSON.stringify(handoff)}\n`, 'utf8');
1846
+ autoQueueMemberNextPush(to, `${from} handoff: ${cleanSummary}`, `Baton pass from ${from}`, parentRoot);
1847
+ } catch {}
1848
+ return handoff;
1849
+ }
1850
+
1851
+ function closeMemberHandoffsForReceiver(receiverMember, root = process.cwd()) {
1852
+ const receiver = String(receiverMember || '').trim().toLowerCase();
1853
+ if (!receiver) return 0;
1854
+ const parentRoot = resolveMemberWorkspaceRoot(root);
1855
+ const handoffsFile = path.join(parentRoot, '.atris', 'state', 'member-handoffs.jsonl');
1856
+ if (!fs.existsSync(handoffsFile)) return 0;
1857
+ try {
1858
+ const rawLines = fs.readFileSync(handoffsFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
1859
+ let closedCount = 0;
1860
+ const now = stampIso();
1861
+ const updatedLines = rawLines.map((line) => {
1862
+ try {
1863
+ const row = JSON.parse(line);
1864
+ if (row && String(row.to || '').toLowerCase() === receiver && (row.status || 'open') === 'open') {
1865
+ closedCount++;
1866
+ return JSON.stringify({ ...row, status: 'closed', closed_at: now });
1867
+ }
1868
+ } catch {}
1869
+ return line;
1870
+ });
1871
+ if (closedCount > 0) {
1872
+ fs.writeFileSync(handoffsFile, `${updatedLines.join('\n')}\n`, 'utf8');
1873
+ }
1874
+ return closedCount;
1875
+ } catch {
1876
+ return 0;
1877
+ }
1878
+ }
1879
+
1689
1880
  function memberMissionFile(owner, root = process.cwd()) {
1690
1881
  const dir = memberDir(owner, root);
1691
1882
  if (!dir) return null;
@@ -2940,6 +3131,7 @@ function missionFromArgs(args) {
2940
3131
  '--cadence',
2941
3132
  '--loop',
2942
3133
  '--runner',
3134
+ '--engine',
2943
3135
  '--lane',
2944
3136
  '--verify',
2945
3137
  '--stop',
@@ -2968,7 +3160,10 @@ function missionFromArgs(args) {
2968
3160
  const maxTicksOverride = readPositiveIntegerFlag(args, '--max-ticks', null, { json: wantsJson(args) });
2969
3161
  const requestedOwner = readFlag(args, '--owner', process.env.ATRIS_AGENT_ID || 'mission-lead');
2970
3162
  const cadence = readFlag(args, '--cadence', readFlag(args, '--loop', 'manual')) || 'manual';
2971
- const runnerSelection = resolveMissionRunnerSelection(readFlag(args, '--runner', 'manual'), { asJson: wantsJson(args), label: 'runner' });
3163
+ const engineFlag = readFlag(args, '--engine', '');
3164
+ const runnerSelection = engineFlag
3165
+ ? resolveMissionRunnerSelection(engineFlag, { asJson: wantsJson(args), engineOnly: true, label: 'engine' })
3166
+ : resolveMissionRunnerSelection(readFlag(args, '--runner', 'manual'), { asJson: wantsJson(args), label: 'runner' });
2972
3167
  const runner = runnerSelection.runner;
2973
3168
  const model = readFlag(args, '--model', '') || (String(runner).toLowerCase() === 'atris2' ? 'atris:fast' : '');
2974
3169
  const lane = readFlag(args, '--lane', 'workspace');
@@ -2985,7 +3180,8 @@ function missionFromArgs(args) {
2985
3180
  assertMissionVerifier(verifier, wantsJson(args));
2986
3181
  const stopCondition = readFlag(args, '--stop', budgetStopCondition(budgetContract) || (verifier ? 'verifier passes and no human asks remain' : 'human marks complete with proof'));
2987
3182
  const taskIds = readRepeatedFlag(args, '--task');
2988
- const humanAsks = readRepeatedFlag(args, '--ask');
3183
+ // MEMBER.md approval-required becomes an open human ask the owner must clear.
3184
+ const humanAsks = [...readRepeatedFlag(args, '--ask'), ...memberApprovalAsks(owner, process.cwd(), objective)];
2989
3185
  const drive = missionDrive(readFlag(args, '--destination', ''));
2990
3186
  const alwaysOn = hasFlag(args, '--always-on');
2991
3187
  const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
@@ -8612,6 +8808,101 @@ function consumeMissionPings(mission, cwd) {
8612
8808
  return { mission: saved, pings: pending };
8613
8809
  }
8614
8810
 
8811
+ function readBoundedContextFile(filePath, maxChars = 1600) {
8812
+ try {
8813
+ if (!fs.existsSync(filePath)) return '';
8814
+ // Frontmatter is machine config (skills, permissions); the prompt only
8815
+ // needs the body.
8816
+ const raw = fs.readFileSync(filePath, 'utf8')
8817
+ .replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/, '')
8818
+ .replace(/<!--[\s\S]*?-->/g, '')
8819
+ .trim();
8820
+ if (!raw) return '';
8821
+ return raw.length > maxChars ? `${raw.slice(0, maxChars).trimEnd()}\n...[truncated]` : raw;
8822
+ } catch {
8823
+ return '';
8824
+ }
8825
+ }
8826
+
8827
+ function buildPreloadedMemberContextLines(mission, root = process.cwd()) {
8828
+ const owner = String(mission?.owner || '').trim();
8829
+ const baseRoot = mission?.worktree_root || mission?.worktree?.path || root;
8830
+ const sections = [];
8831
+ if (owner && /^[a-zA-Z0-9._-]+$/.test(owner)) {
8832
+ const teamMemberDir = memberDir(owner, baseRoot) || path.join(baseRoot, 'atris', 'team', owner);
8833
+ const memberMd = readBoundedContextFile(path.join(teamMemberDir, 'MEMBER.md'), 1800);
8834
+ const soulMd = readBoundedContextFile(path.join(teamMemberDir, 'SOUL.md'), 1200);
8835
+ const missionMd = readBoundedContextFile(path.join(teamMemberDir, 'MISSION.md'), 800);
8836
+ if (memberMd) {
8837
+ sections.push(`### Member Identity (atris/team/${owner}/MEMBER.md)`, memberMd);
8838
+ }
8839
+ if (soulMd) {
8840
+ sections.push(`### Member Soul (atris/team/${owner}/SOUL.md)`, soulMd);
8841
+ }
8842
+ if (missionMd) {
8843
+ sections.push(`### Member Standing Mission (atris/team/${owner}/MISSION.md)`, missionMd);
8844
+ }
8845
+ try {
8846
+ const skillsDir = path.join(teamMemberDir, 'skills');
8847
+ if (fs.existsSync(skillsDir)) {
8848
+ const skillSnippets = fs.readdirSync(skillsDir)
8849
+ .sort()
8850
+ .slice(0, 3)
8851
+ .map((slug) => {
8852
+ const skillText = readBoundedContextFile(path.join(skillsDir, slug, 'SKILL.md'), 500);
8853
+ return skillText ? `#### Skill: ${slug}\n${skillText}` : '';
8854
+ })
8855
+ .filter(Boolean);
8856
+ if (skillSnippets.length) {
8857
+ sections.push(`### Unlocked Member Skills (atris/team/${owner}/skills/)`, skillSnippets.join('\n\n'));
8858
+ }
8859
+ }
8860
+ } catch {}
8861
+ try {
8862
+ const logsDir = path.join(teamMemberDir, 'logs');
8863
+ if (fs.existsSync(logsDir)) {
8864
+ const latestLog = fs.readdirSync(logsDir)
8865
+ .filter((name) => name.endsWith('.md'))
8866
+ .sort()
8867
+ .pop();
8868
+ if (latestLog) {
8869
+ const rawLog = fs.readFileSync(path.join(logsDir, latestLog), 'utf8').trim();
8870
+ if (rawLog) {
8871
+ const tail = rawLog.length > 500 ? `...[earlier entries]\n${rawLog.slice(-500).trimStart()}` : rawLog;
8872
+ sections.push(`### Recent Member Memory (atris/team/${owner}/logs/${latestLog})`, tail);
8873
+ }
8874
+ }
8875
+ }
8876
+ } catch {}
8877
+ }
8878
+ const boundTask = mission?.xp_task || mission?.selected_target_task || null;
8879
+ if (boundTask && (boundTask.title || boundTask.ref || boundTask.task_id)) {
8880
+ const taskRef = boundTask.ref || boundTask.task_ref || boundTask.task_id;
8881
+ const taskTitle = boundTask.title ? `: ${boundTask.title}` : '';
8882
+ const taskStatus = boundTask.status ? ` (${boundTask.status})` : '';
8883
+ sections.push(`### Bound Task Context`, `- Active task: ${taskRef}${taskTitle}${taskStatus}`);
8884
+ }
8885
+ try {
8886
+ const tasteHistory = readTasteReviewHistory(baseRoot);
8887
+ const tasteLines = [];
8888
+ if (tasteHistory?.accepted?.length) {
8889
+ tasteLines.push(...tasteHistory.accepted.slice(0, 2).map((line) => `- Accepted: ${line}`));
8890
+ }
8891
+ if (tasteHistory?.revised?.length) {
8892
+ tasteLines.push(...tasteHistory.revised.slice(0, 2).map((line) => `- Sent back: ${line}`));
8893
+ }
8894
+ if (tasteLines.length) {
8895
+ sections.push(`### Operator Taste & Review Notes`, tasteLines.join('\n'));
8896
+ }
8897
+ } catch {}
8898
+ if (!sections.length) return [];
8899
+ return [
8900
+ ``,
8901
+ `## Preloaded Member & Task Context`,
8902
+ ...sections,
8903
+ ];
8904
+ }
8905
+
8615
8906
  function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd = process.cwd()) {
8616
8907
  const sharedProcess = memberProcessPrompt(cwd);
8617
8908
  const pingLines = pings.length
@@ -8631,6 +8922,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
8631
8922
  checkFeedback,
8632
8923
  ]
8633
8924
  : [];
8925
+ const preloadedContextLines = buildPreloadedMemberContextLines(mission, cwd);
8634
8926
  const lines = [
8635
8927
  `# Mission Tick ${tickIndex}/${maxTicks}`,
8636
8928
  ...pingLines,
@@ -8646,6 +8938,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
8646
8938
  ...missionBudgetPromptLines(mission),
8647
8939
  ...checkFeedbackLines,
8648
8940
  ...(sharedProcess ? ['', sharedProcess] : []),
8941
+ ...preloadedContextLines,
8649
8942
  ``,
8650
8943
  `## Your task`,
8651
8944
  `Do ONE increment of work toward the stop condition. ONE. No more.`,
@@ -8657,6 +8950,7 @@ function buildTickPrompt(mission, tickIndex, maxTicks, frozen, pings = [], cwd =
8657
8950
  `- The tick is not done until the verify step passes. If a frozen verifier exists, the harness runs it, but that does not replace the real-surface proof below. If no frozen verifier exists, finish by actually running the surface you changed on this computer and paste the real command output into your receipt.`,
8658
8951
  `- After that one verify step, STOP. Never start a second slice in one tick.`,
8659
8952
  `- Write the tick summary in operator language: what changed and what it buys or costs in plain words, with no flags, task ids, or code identifiers.`,
8953
+ `- Propose the next needle-mover: include a \`next_push: <one concrete feature or optimization to build next>\` line in your receipt and member log so the team compounds exponentially.`,
8660
8954
  ``,
8661
8955
  `## No claimable work? Work the backstop ladder`,
8662
8956
  `If the mission and task state offer no claimable work, inspect these rungs in order. Pick the FIRST rung with real fuel and do ONE bounded slice:`,
@@ -9217,10 +9511,25 @@ function engineVerifierResultFromRun(engineResult, engine = 'codex') {
9217
9511
  };
9218
9512
  }
9219
9513
 
9514
+ function pickCrossEngineVerifier(mission, cwd = process.cwd(), preferredEngine = '') {
9515
+ const explicit = canonicalEngineName(preferredEngine || mission?.referee_engine || '');
9516
+ if (explicit) return explicit;
9517
+ const workerEngine = canonicalEngineName(mission?.runner) || 'claude';
9518
+ const candidates = workerEngine === 'codex' ? ['claude', 'fable', 'codex'] : ['codex', 'claude'];
9519
+ try {
9520
+ const engines = engineRegistryView(cwd);
9521
+ for (const candidate of candidates) {
9522
+ const entry = engines.find((e) => e.id === candidate);
9523
+ if (entry && entry.installed && entry.health?.status === 'ready') return candidate;
9524
+ }
9525
+ } catch {}
9526
+ return workerEngine === 'codex' ? 'claude' : 'codex';
9527
+ }
9528
+
9220
9529
  async function runEngineVerifier(mission, options = {}) {
9221
9530
  const cwd = options.cwd || process.cwd();
9222
- const engine = 'codex';
9223
- const verifyMission = { ...mission, runner: engine, runner_kind: 'engine' };
9531
+ const engine = pickCrossEngineVerifier(mission, cwd, options.refereeEngine);
9532
+ const verifyMission = { ...mission, runner: engine, runner_kind: 'engine', model: '' };
9224
9533
  const restoreRunnerProfile = applyMissionRunnerProfile(engine);
9225
9534
  let engineResult;
9226
9535
  try {
@@ -9261,6 +9570,22 @@ function usefulClaudeReceiptSummary(text, fallback = 'no-text') {
9261
9570
  return fallback;
9262
9571
  }
9263
9572
 
9573
+ function extractNextPushFromReceiptText(text) {
9574
+ const raw = String(text || '');
9575
+ if (!raw.trim()) return null;
9576
+ const explicitMatch = raw.match(/^\s*(?:[-*]\s*)?(?:next_push|next push|next needle[- ]mover)\s*:\s*(.+)$/im);
9577
+ if (explicitMatch && explicitMatch[1]) {
9578
+ const cleaned = explicitMatch[1].replace(/`/g, '').replace(/\s+/g, ' ').trim();
9579
+ if (cleaned && !/^<one concrete/i.test(cleaned)) return cleaned.slice(0, 180);
9580
+ }
9581
+ const numberedMatch = raw.match(/^\s*(?:\(3\)|3[.)])\s*(?:next(?:\s+tick)?\s*:\s*)?(.+)$/im);
9582
+ if (numberedMatch && numberedMatch[1]) {
9583
+ const cleaned = numberedMatch[1].replace(/`/g, '').replace(/\s+/g, ' ').trim();
9584
+ if (cleaned && !/^layer\s*:/i.test(cleaned)) return cleaned.slice(0, 180);
9585
+ }
9586
+ return null;
9587
+ }
9588
+
9264
9589
  function cappedClaudeReceiptText(text, limit = 4000) {
9265
9590
  const clean = String(text || '').trim();
9266
9591
  if (!clean) return '';
@@ -9316,6 +9641,8 @@ async function parseAndValidateMissionRunPhase(args) {
9316
9641
  const selfDrive = dueMode || headlessOnly || hasFlag(args, '--self-drive');
9317
9642
  const skipClaude = hasFlag(args, '--no-claude');
9318
9643
  const verifyEach = !hasFlag(args, '--no-verify');
9644
+ const refereeEach = hasFlag(args, '--referee') && !hasFlag(args, '--no-referee');
9645
+ const refereeEngine = readFlag(args, '--referee-engine', '');
9319
9646
  const completeOnPass = hasFlag(args, '--complete-on-pass');
9320
9647
  const skipDrain = hasFlag(args, '--no-drain');
9321
9648
  const createNext = hasFlag(args, '--create-next');
@@ -9351,6 +9678,8 @@ async function parseAndValidateMissionRunPhase(args) {
9351
9678
  selfDrive,
9352
9679
  skipClaude,
9353
9680
  verifyEach,
9681
+ refereeEach,
9682
+ refereeEngine,
9354
9683
  completeOnPass,
9355
9684
  skipDrain,
9356
9685
  createNext,
@@ -9376,7 +9705,6 @@ async function resolveMissionRunPhase(context) {
9376
9705
  const {
9377
9706
  args,
9378
9707
  asJson,
9379
- cwd,
9380
9708
  dueMode,
9381
9709
  headlessOnly,
9382
9710
  budgetTier,
@@ -9389,7 +9717,7 @@ async function resolveMissionRunPhase(context) {
9389
9717
  ref,
9390
9718
  runArgs,
9391
9719
  } = context;
9392
- let { maxTicks, maxWallSeconds } = context;
9720
+ let { cwd, maxTicks, maxWallSeconds } = context;
9393
9721
 
9394
9722
  if (!dueMode && !ref) {
9395
9723
  if (asJson || !process.stdin.isTTY || !process.stderr.isTTY) {
@@ -9428,6 +9756,13 @@ async function resolveMissionRunPhase(context) {
9428
9756
  if (!mission) {
9429
9757
  exitMissionError('Usage: atris mission run <id|objective> [--max-ticks 4] [--max-wall 3600]', 1, asJson);
9430
9758
  }
9759
+ const worktreeRoot = mission.worktree_root || mission.worktree?.path || '';
9760
+ if (worktreeRoot && fs.existsSync(worktreeRoot) && path.resolve(worktreeRoot) !== path.resolve(cwd)) {
9761
+ cwd = path.resolve(worktreeRoot);
9762
+ context.cwd = cwd;
9763
+ process.chdir(cwd);
9764
+ mission = resolveMission(mission.id, cwd) || mission;
9765
+ }
9431
9766
  if (!maxTicksFlag && !budgetTier && Number(mission.max_ticks) > 0) {
9432
9767
  maxTicks = Math.max(1, Number(mission.max_ticks));
9433
9768
  }
@@ -9452,6 +9787,30 @@ async function resolveMissionRunPhase(context) {
9452
9787
  console.error(`Mission ${mission.id} is ${mission.status}; nothing to run.`);
9453
9788
  process.exit(0);
9454
9789
  }
9790
+ // An explicit run must respect open human asks the same way auto-selection
9791
+ // does (missionIsRunnable), instead of ticking straight past them.
9792
+ const blockingAsks = runBlockingAsks(mission.human_asks);
9793
+ if (blockingAsks.length) {
9794
+ detachedDriverLifecycle?.setExitReason('human_asks_open');
9795
+ const lines = [
9796
+ `Mission ${mission.id} is waiting on ${blockingAsks.length} human ask${blockingAsks.length === 1 ? '' : 's'}; not ticking.`,
9797
+ ...blockingAsks.map((ask) => ` - ${ask.text}${ask.answered_at ? ' (answered no)' : ''}`),
9798
+ 'Answer with: atris decide (then atris decide <n> y), and re-run.',
9799
+ ];
9800
+ if (asJson) {
9801
+ printJsonOrText({
9802
+ ok: false,
9803
+ action: 'run_blocked',
9804
+ reason: 'human_asks_open',
9805
+ mission_id: mission.id,
9806
+ human_asks: blockingAsks.map((ask) => ask.text),
9807
+ next_command: 'atris decide',
9808
+ }, [], true);
9809
+ } else {
9810
+ for (const line of lines) console.error(line);
9811
+ }
9812
+ process.exit(2);
9813
+ }
9455
9814
  if (hasFlag(args, '--detach')) {
9456
9815
  detachMissionRun(args, mission, cwd, asJson);
9457
9816
  context.handled = true;
@@ -9671,6 +10030,7 @@ async function executeMissionRunTicksPhase(context) {
9671
10030
  const tickEngineId = engineBackedTick ? (tickSelection.engine_id || inferredEngineId) : null;
9672
10031
  if (engineBackedTick) {
9673
10032
  result.engine_id = tickEngineId;
10033
+ if (tickRuntimeMission.model) result.model = tickRuntimeMission.model;
9674
10034
  result.requested_engine = tickSelection.requested_engine;
9675
10035
  result.engine_fallback_reason = tickSelection.engine_fallback_reason;
9676
10036
  }
@@ -9945,6 +10305,7 @@ async function executeMissionRunTicksPhase(context) {
9945
10305
  tickSkipWorker,
9946
10306
  tickIdx,
9947
10307
  frozen,
10308
+ storedVerifier,
9948
10309
  };
9949
10310
  let verifierResult = null;
9950
10311
  if (cachedStep) {
@@ -9986,12 +10347,15 @@ async function executeMissionRunTicksPhase(context) {
9986
10347
 
9987
10348
  // Layer classification needs the receipt text AND the worktree receipt, so it
9988
10349
  // runs here, after both exist, covering the claude and atris2 branches alike.
10350
+ const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
9989
10351
  if (!(cachedStep && cachedStep.tick && cachedStep.tick.layer)) {
9990
- const tickReceiptText = result.atris2?.receipt_text || result.claude?.receipt_text || result.drill?.receipt_text || '';
9991
10352
  const layerInfo = extractLayerFromReceiptText(tickReceiptText, tickWorktree?.new_since_baseline_sample);
9992
10353
  result.layer = layerInfo.layer;
9993
10354
  result.layer_source = layerInfo.source;
9994
10355
  }
10356
+ if (!result.next_push) {
10357
+ result.next_push = extractNextPushFromReceiptText(tickReceiptText);
10358
+ }
9995
10359
 
9996
10360
  // Persist tick to mission state + write structured receipt
9997
10361
  const finishedAt = stampIso();
@@ -10017,9 +10381,14 @@ async function executeMissionRunTicksPhase(context) {
10017
10381
  // reopen closed rooms, mission 32 complete@19:23:33 → ready@19:23:48).
10018
10382
  const latestOnDisk = loadMissionMap(cwd).get(mission.id) || mission;
10019
10383
  const alreadyTerminal = TERMINAL_STATUSES.has(String(latestOnDisk.status || ''));
10384
+ // A passing verifier alone never completes a mission whose linked task
10385
+ // is not accepted: park it ready and name the accept command instead.
10386
+ const acceptGate = (verifierResult?.passed && completeOnPass && !fullBudgetMode && !mission.always_on && !xpReadyAction)
10387
+ ? missionLinkedTaskGate(mission, cwd)
10388
+ : { ok: true };
10020
10389
  let newStatus = (verifierResult?.passed && mission.always_on) ? 'ready' :
10021
10390
  (verifierResult?.passed && xpReadyAction) ? 'ready' :
10022
- (verifierResult?.passed && completeOnPass && !fullBudgetMode) ? 'complete' :
10391
+ (verifierResult?.passed && completeOnPass && !fullBudgetMode && acceptGate.ok) ? 'complete' :
10023
10392
  (verifierResult?.passed && fullBudgetMode) ? 'running' :
10024
10393
  (verifierResult?.passed ? 'ready' :
10025
10394
  (verifierResult ? 'blocked' :
@@ -10033,6 +10402,8 @@ async function executeMissionRunTicksPhase(context) {
10033
10402
  nextAction = nextCandidateTickAction(mission);
10034
10403
  } else if (verifierResult?.passed && xpReadyAction) {
10035
10404
  nextAction = xpReadyAction;
10405
+ } else if (verifierResult?.passed && completeOnPass && !fullBudgetMode && !acceptGate.ok) {
10406
+ nextAction = missionAcceptNextAction(mission, acceptGate);
10036
10407
  } else if (verifierResult?.passed && completeOnPass && !fullBudgetMode) {
10037
10408
  nextAction = 'mission complete';
10038
10409
  } else if (verifierResult?.passed && fullBudgetMode) {
@@ -10070,6 +10441,7 @@ async function executeMissionRunTicksPhase(context) {
10070
10441
  last_tick_layer: result.layer,
10071
10442
  last_tick_layer_source: result.layer_source,
10072
10443
  error_streak_count: errorStreakCount,
10444
+ last_next_push: result.next_push || latestOnDisk.last_next_push || null,
10073
10445
  verifier_result: verifierResult || (verifyEach && result.protected_lane_guard && result.protected_lane_guard.allowed === false ? null : latestOnDisk.verifier_result) || null,
10074
10446
  last_check_feedback: verifierResult
10075
10447
  ? extractCheckFeedback(verifierResult)
@@ -10078,7 +10450,16 @@ async function executeMissionRunTicksPhase(context) {
10078
10450
  next_action: nextAction,
10079
10451
  claude_session_ticks: rotateSessionForContext ? 0 : claudeSessionTicks,
10080
10452
  }, cwd, 'mission_tick', {
10081
- tick_index: tickIdx, status: result.status, reason: result.reason, receipt_path: receiptPath, layer: result.layer,
10453
+ tick_index: tickIdx,
10454
+ status: result.status,
10455
+ reason: result.reason,
10456
+ receipt_path: receiptPath,
10457
+ layer: result.layer,
10458
+ next_push: result.next_push || undefined,
10459
+ engine: result.engine_id || runtimeMission.runner || mission.runner || null,
10460
+ task_type: 'executor',
10461
+ verified_passed: verifierResult && typeof verifierResult.passed === 'boolean' ? verifierResult.passed : undefined,
10462
+ duration_ms: Math.max(0, (Date.parse(finishedAt) || 0) - (Date.parse(tickStart) || 0)),
10082
10463
  }).mission;
10083
10464
  if (rotateSessionForContext) {
10084
10465
  // Same shape as the stale-lock rotation above: mint the fresh pending
@@ -10093,6 +10474,7 @@ async function executeMissionRunTicksPhase(context) {
10093
10474
  },
10094
10475
  ).mission;
10095
10476
  }
10477
+ const tickSummaryText = String(result?.claude?.summary || '').replace(/\s+/g, ' ').trim();
10096
10478
  appendMemberLog(mission.owner, `Mission run tick ${tickIdx}`, {
10097
10479
  mission: mission.objective,
10098
10480
  state: mission.status,
@@ -10100,8 +10482,39 @@ async function executeMissionRunTicksPhase(context) {
10100
10482
  reason: result.reason,
10101
10483
  layer: result.layer || undefined,
10102
10484
  verifier: verifierResult ? (verifierResult.passed ? 'passed' : 'failed') : 'not_run',
10485
+ summary: tickSummaryText ? tickSummaryText.slice(0, 240) : undefined,
10486
+ next_push: result.next_push ? String(result.next_push).slice(0, 180) : undefined,
10487
+ feedback: verifierResult && !verifierResult.passed ? String(mission.last_check_feedback || '').slice(0, 240) : undefined,
10103
10488
  receipt: receiptPath,
10104
- });
10489
+ }, cwd);
10490
+ if (verifierResult && verifierResult.passed === true && result.status === 'ran') {
10491
+ try {
10492
+ crystallizeMemberSkillLesson(mission.owner, {
10493
+ objective: mission.objective,
10494
+ summary: tickSummaryText || mission.objective,
10495
+ verifier: frozen.verifier || mission.verifier || '',
10496
+ next_push: result.next_push || '',
10497
+ layer: result.layer || 'capabilities',
10498
+ }, cwd);
10499
+ } catch {}
10500
+ if (result.next_push) {
10501
+ try {
10502
+ autoQueueMemberNextPush(mission.owner, result.next_push, mission.objective, cwd, {
10503
+ promote: newStatus === 'complete',
10504
+ });
10505
+ } catch {}
10506
+ }
10507
+ if (mission.owner) {
10508
+ try {
10509
+ closeMemberHandoffsForReceiver(mission.owner, cwd);
10510
+ } catch {}
10511
+ }
10512
+ if (newStatus === 'complete' && mission.owner) {
10513
+ try {
10514
+ recordMemberHandoff(mission.owner, '', tickSummaryText || mission.objective, cwd);
10515
+ } catch {}
10516
+ }
10517
+ }
10105
10518
  if (newStatus === 'complete') {
10106
10519
  continuationGoal = seedMissionRunContinuation(mission, cwd, receiptPath);
10107
10520
  if (continuationGoal?.parent) mission = continuationGoal.parent;
@@ -10262,12 +10675,37 @@ async function verifyMissionRunTickPhase(context) {
10262
10675
  if (result.status === 'ran' && verifyEach) {
10263
10676
  if (currentTick.frozen.verifier) {
10264
10677
  verifierResult = runVerifier(currentTick.frozen.verifier);
10265
- } else if (!currentTick.tickSkipWorker) {
10266
- verifierResult = await runEngineVerifier(currentTick.tickRuntimeMission, {
10678
+ }
10679
+ const shouldRunReferee = !currentTick.tickSkipWorker
10680
+ && (!verifierResult || verifierResult.passed)
10681
+ && (
10682
+ !currentTick.frozen.verifier
10683
+ || context.refereeEach
10684
+ || (!currentTick.storedVerifier && !process.env.NODE_TEST_CONTEXT)
10685
+ );
10686
+ if (shouldRunReferee) {
10687
+ const refereeResult = await runEngineVerifier(currentTick.tickRuntimeMission, {
10267
10688
  cwd,
10268
10689
  signal: controller.signal,
10269
10690
  tickIndex: currentTick.tickIdx,
10691
+ refereeEngine: context.refereeEngine,
10270
10692
  });
10693
+ result.referee_result = refereeResult;
10694
+ if (!verifierResult || !refereeResult.passed) {
10695
+ verifierResult = verifierResult
10696
+ ? {
10697
+ ...verifierResult,
10698
+ passed: Boolean(verifierResult.passed && refereeResult.passed),
10699
+ referee: refereeResult,
10700
+ output: [verifierResult.stdout, verifierResult.stderr, refereeResult.output].filter(Boolean).join('\n'),
10701
+ }
10702
+ : refereeResult;
10703
+ } else {
10704
+ verifierResult = {
10705
+ ...verifierResult,
10706
+ referee: refereeResult,
10707
+ };
10708
+ }
10271
10709
  }
10272
10710
  if (verifierResult) result.verifier_passed = verifierResult.passed;
10273
10711
  }
@@ -10601,10 +11039,14 @@ function tickMission(args) {
10601
11039
  const xpReadyAction = missionXpReadyAction(mission, receiptPath);
10602
11040
  const budgetRemainingSeconds = missionFullBudgetRemainingSeconds(mission);
10603
11041
  const budgetOpen = budgetRemainingSeconds > 0;
10604
- status = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen) ? 'complete'
11042
+ const acceptGate = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen)
11043
+ ? missionLinkedTaskGate(mission, process.cwd())
11044
+ : { ok: true };
11045
+ status = (completeOnPass && !mission.always_on && !xpReadyAction && !budgetOpen && acceptGate.ok) ? 'complete'
10605
11046
  : (budgetOpen && !mission.always_on && !xpReadyAction ? 'running' : 'ready');
10606
11047
  nextAction = mission.always_on ? nextCandidateTickAction(mission) :
10607
11048
  (xpReadyAction
11049
+ || (!acceptGate.ok ? missionAcceptNextAction(mission, acceptGate) : null)
10608
11050
  || (budgetOpen
10609
11051
  ? `proof passed with ${durationLabel(budgetRemainingSeconds)} left on the budget; pick the next useful move, then: atris mission tick ${mission.id} --verify --summary "<what changed>"`
10610
11052
  : (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`)));
@@ -10747,6 +11189,38 @@ function receiptShowsPass(receipt) {
10747
11189
  // verifier passed. Mirrors the task plane's proof-only accept guard so the
10748
11190
  // final transition consumes the receipts instead of trusting free text.
10749
11191
  function missionCompletionGate(mission, proof, root = process.cwd()) {
11192
+ const base = missionVerifierCompletionGate(mission, proof, root);
11193
+ if (!base.ok) {
11194
+ // The check not passing is the first thing to fix, but a linked task that
11195
+ // is not accepted would refuse the next try too: say so now.
11196
+ const pending = missionLinkedTaskGate(mission, root);
11197
+ if (pending.ok) return base;
11198
+ const how = pending.accept_command ? ` (${pending.accept_command})` : '';
11199
+ return { ...base, linked_task_note: `Also, ${pending.reason}${how}` };
11200
+ }
11201
+ // Done means an accepted task plus a receipt: a passing verifier is not
11202
+ // enough when the mission is linked to task(s) a human has not accepted.
11203
+ const linked = missionLinkedTaskGate(mission, root);
11204
+ if (!linked.ok) {
11205
+ return {
11206
+ ok: false,
11207
+ source: 'linked_task',
11208
+ reason: linked.reason,
11209
+ task_id: linked.task_id,
11210
+ task_ref: linked.task_ref,
11211
+ accept_command: linked.accept_command,
11212
+ archived_only: linked.archived_only === true,
11213
+ };
11214
+ }
11215
+ return linked.tasks.length ? { ...base, linked_tasks: linked.tasks } : base;
11216
+ }
11217
+
11218
+ function missionAcceptNextAction(mission, gate) {
11219
+ const how = gate.accept_command || (gate.archived_only ? 'link a live task and accept it' : 'attach a receipt to it');
11220
+ return `verifier passed but ${gate.reason}; accept it (${how}), then: atris mission complete ${mission.id} --proof "<receipt>"`;
11221
+ }
11222
+
11223
+ function missionVerifierCompletionGate(mission, proof, root = process.cwd()) {
10750
11224
  const remainingSeconds = missionFullBudgetRemainingSeconds(mission);
10751
11225
  if (remainingSeconds > 0) {
10752
11226
  return {
@@ -10789,10 +11263,19 @@ function completeMission(args) {
10789
11263
  }
10790
11264
  const gate = missionCompletionGate(mission, proof, root);
10791
11265
  if (!gate.ok && !force) {
10792
- exitMissionError(`[mission complete] ${gate.reason}. Run: atris mission tick ${mission.id} --verify (or override as operator with --force)`, 2, asJson);
11266
+ const fix = gate.source === 'linked_task'
11267
+ ? (gate.accept_command
11268
+ ? `Accept it first: ${gate.accept_command}, then re-run mission complete`
11269
+ : (gate.archived_only ? 'Link a live task to the mission and accept it first' : 'Attach a receipt to it first'))
11270
+ : `Run: atris mission tick ${mission.id} --verify`;
11271
+ const also = gate.linked_task_note ? ` ${gate.linked_task_note}.` : '';
11272
+ exitMissionError(`[mission complete] ${gate.reason}. ${fix}.${also} (or override as operator with --force)`, 2, asJson);
11273
+ }
11274
+ if (!gate.ok && force && gate.source === 'linked_task') {
11275
+ console.error(`[mission complete] WARNING --force overrides the accepted-task gate: ${gate.reason}. Recorded as completion_gate.forced.`);
10793
11276
  }
10794
11277
  const baselineSummary = pruneMissionWorktreeBaseline(mission, root);
10795
- const completionGate = { ...gate, forced: force && !gate.ok };
11278
+ const completionGate = { ...gate, forced: force && !gate.ok, ...(force && !gate.ok ? { forced_reason: gate.reason } : {}) };
10796
11279
  const baseNext = {
10797
11280
  ...mission,
10798
11281
  status: 'complete',
@@ -11849,6 +12332,7 @@ module.exports = {
11849
12332
  startMission,
11850
12333
  resolveMission,
11851
12334
  spawnMissionDriver,
12335
+ missionDriverHealth,
11852
12336
  missionRunnerNeedsLiveSession,
11853
12337
  completeMission,
11854
12338
  inspectMission,
@@ -11874,6 +12358,9 @@ module.exports = {
11874
12358
  selectCodexGoalMission,
11875
12359
  codexGoalNextCommand,
11876
12360
  usefulClaudeReceiptSummary,
12361
+ autoQueueMemberNextPush,
12362
+ recordMemberHandoff,
12363
+ closeMemberHandoffsForReceiver,
11877
12364
  cappedClaudeReceiptText,
11878
12365
  extractLayerFromReceiptText,
11879
12366
  classifyPathsByLayer,
@@ -11893,6 +12380,7 @@ module.exports = {
11893
12380
  missionVerifierHighLevelTestText,
11894
12381
  buildEngineVerifyPrompt,
11895
12382
  engineVerifierResultFromRun,
12383
+ pickCrossEngineVerifier,
11896
12384
  runVerifier,
11897
12385
  missionFullBudgetRemainingSeconds,
11898
12386
  missionBudgetContinuationText,