atris 3.37.1 → 3.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +2 -0
  2. package/atris/PERSONA.md +3 -7
  3. package/atris.md +10 -2
  4. package/ax +158 -33
  5. package/bin/atris.js +49 -2
  6. package/commands/activate.js +16 -3
  7. package/commands/agent-spawn.js +0 -7
  8. package/commands/autoland.js +92 -14
  9. package/commands/autopilot-front.js +0 -4
  10. package/commands/autopilot.js +0 -12
  11. package/commands/brain.js +0 -7
  12. package/commands/brief.js +0 -2
  13. package/commands/business-sync.js +0 -1
  14. package/commands/business.js +107 -12
  15. package/commands/clean.js +31 -2
  16. package/commands/close.js +46 -11
  17. package/commands/cloud.js +0 -1
  18. package/commands/codex-goal.js +0 -2
  19. package/commands/computer.js +6 -5
  20. package/commands/console.js +46 -9
  21. package/commands/deck.js +29 -8
  22. package/commands/dream.js +0 -7
  23. package/commands/drill.js +0 -1
  24. package/commands/engine.js +10 -18
  25. package/commands/experiments.js +0 -4
  26. package/commands/fleet-report.js +12 -4
  27. package/commands/game.js +261 -0
  28. package/commands/gm.js +10 -2
  29. package/commands/goal.js +0 -6
  30. package/commands/harvest.js +0 -1
  31. package/commands/improve.js +29 -8
  32. package/commands/init.js +18 -34
  33. package/commands/integrations.js +0 -7
  34. package/commands/land.js +71 -8
  35. package/commands/launchpad.js +0 -2
  36. package/commands/lesson.js +17 -7
  37. package/commands/loop-front.js +0 -2
  38. package/commands/loops.js +6 -5
  39. package/commands/meet.js +0 -2
  40. package/commands/member.js +68 -3
  41. package/commands/mission.js +359 -102
  42. package/commands/next.js +0 -1
  43. package/commands/now.js +0 -12
  44. package/commands/one-lap.js +8 -8
  45. package/commands/orb.js +0 -4
  46. package/commands/pack-craft.js +0 -1
  47. package/commands/pack.js +670 -77
  48. package/commands/play.js +0 -2
  49. package/commands/probe.js +10 -4
  50. package/commands/pulse.js +73 -48
  51. package/commands/push.js +0 -4
  52. package/commands/radar.js +14 -11
  53. package/commands/recover.js +340 -0
  54. package/commands/report.js +0 -1
  55. package/commands/router.js +319 -0
  56. package/commands/run-front.js +0 -2
  57. package/commands/search.js +0 -3
  58. package/commands/skill.js +7 -2
  59. package/commands/slop.js +225 -7
  60. package/commands/status.js +6 -0
  61. package/commands/stream.js +0 -2
  62. package/commands/study.js +0 -3
  63. package/commands/sync-checkout.js +51 -0
  64. package/commands/sync.js +11 -40
  65. package/commands/task.js +120 -38
  66. package/commands/teach.js +86 -0
  67. package/commands/unknowns.js +0 -10
  68. package/commands/update.js +0 -1
  69. package/commands/visualize.js +0 -5
  70. package/commands/watch.js +0 -6
  71. package/commands/wiki.js +41 -11
  72. package/commands/wish.js +0 -1
  73. package/commands/worktree.js +87 -14
  74. package/commands/xp.js +1 -6
  75. package/commands/youtube.js +0 -2
  76. package/lib/accept-verify-gate.js +122 -0
  77. package/lib/activity-stream.js +0 -2
  78. package/lib/auto-accept-certified.js +145 -2
  79. package/lib/autoland.js +0 -3
  80. package/lib/ax-auto-lane.js +68 -7
  81. package/lib/bench/context.js +0 -2
  82. package/lib/bench/report.js +0 -1
  83. package/lib/bench/runner.js +0 -11
  84. package/lib/brief-ledger.js +0 -6
  85. package/lib/business-simulate.js +382 -0
  86. package/lib/chat-log-scan.js +0 -5
  87. package/lib/checkout-sync.js +52 -0
  88. package/lib/claude-boot-block.js +48 -0
  89. package/lib/cloud-mission.js +0 -6
  90. package/lib/codex-flight.js +0 -1
  91. package/lib/conductor-artifacts.js +25 -0
  92. package/lib/context-gatherer.js +0 -1
  93. package/lib/deck-compose.js +0 -1
  94. package/lib/deck-history.js +0 -1
  95. package/lib/deck-layout.js +0 -5
  96. package/lib/deck-review.js +0 -7
  97. package/lib/deck-schema.js +0 -1
  98. package/lib/default-verifier.js +0 -2
  99. package/lib/engine-registry.js +16 -8
  100. package/lib/experiments/daily.js +0 -6
  101. package/lib/fleet.js +210 -27
  102. package/lib/functional-owner.js +0 -3
  103. package/lib/inspect-fields.js +0 -1
  104. package/lib/known-commands.js +2 -2
  105. package/lib/learnings.js +0 -2
  106. package/lib/loop-doctor.js +11 -17
  107. package/lib/manifest.js +0 -2
  108. package/lib/member-alive.js +0 -1
  109. package/lib/member-switches.js +0 -5
  110. package/lib/mission-artifact.js +0 -1
  111. package/lib/mission-protected-lane.js +293 -0
  112. package/lib/mission-room.js +0 -2
  113. package/lib/mission-runtime-loop.js +0 -1
  114. package/lib/next-moves.js +0 -18
  115. package/lib/official-cli-integration.js +0 -4
  116. package/lib/orb-context.js +6 -3
  117. package/lib/orb-scorecard.js +0 -1
  118. package/lib/outbound-send-gate.js +0 -6
  119. package/lib/permission-grants.js +0 -6
  120. package/lib/policy-lessons.js +0 -2
  121. package/lib/pulse.js +68 -15
  122. package/lib/review-integrity.js +0 -2
  123. package/lib/router-brain.js +0 -2
  124. package/lib/runner-command.js +0 -4
  125. package/lib/runtime-bootstrap.js +0 -1
  126. package/lib/scorecard.js +0 -1
  127. package/lib/self-drive.js +1 -1
  128. package/lib/state-detection.js +0 -6
  129. package/lib/task-db.js +0 -4
  130. package/lib/team-presence.js +0 -1
  131. package/lib/voice-gate.js +63 -2
  132. package/lib/wiki.js +152 -9
  133. package/lib/wish-audit.js +20 -6
  134. package/lib/wish-delegate.js +26 -20
  135. package/lib/wish-stats.js +0 -5
  136. package/lib/wish-store.js +0 -1
  137. package/lib/workspace-safety.js +0 -2
  138. package/lib/zip.js +0 -1
  139. package/package.json +1 -1
@@ -78,6 +78,11 @@ const {
78
78
  resolveDefaultVerifier,
79
79
  } = require('../lib/default-verifier');
80
80
  const { redirectToWorkspaceRoot } = require('../lib/mission-root');
81
+ const {
82
+ inspectMissionProtectedDiff,
83
+ prepareMissionGitGuard,
84
+ unreadableMissionGuard,
85
+ } = require('../lib/mission-protected-lane');
81
86
 
82
87
  const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
83
88
  const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
@@ -1697,6 +1702,16 @@ function missionLastStepSummary(ticks = []) {
1697
1702
  return '';
1698
1703
  }
1699
1704
 
1705
+ function verifierElapsedSeconds(verifierResult) {
1706
+ const ms = Number(verifierResult && verifierResult.elapsed_ms);
1707
+ return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
1708
+ }
1709
+
1710
+ function verifierTimeoutSeconds(verifierResult) {
1711
+ const ms = Number(verifierResult && verifierResult.timeout_ms);
1712
+ return Number.isFinite(ms) ? Math.round(ms / 1000) : 0;
1713
+ }
1714
+
1700
1715
  function missionVerifierCheckedText(verifierResult, mission) {
1701
1716
  if (!verifierResult) return 'UNVERIFIED: tick recorded but nothing was checked; treat this increment as unproven.';
1702
1717
  if (verifierResult.mode === 'engine-unavailable') {
@@ -1708,6 +1723,9 @@ function missionVerifierCheckedText(verifierResult, mission) {
1708
1723
  : 'VERIFY FAILED: the engine verify pass did not end with VERDICT: PASS.';
1709
1724
  }
1710
1725
  const command = verifierResult.command || mission.verifier || 'configured verifier';
1726
+ if (!verifierResult.passed && verifierResult.timed_out) {
1727
+ return `VERIFY TIMED OUT: the check ran past its ${verifierTimeoutSeconds(verifierResult)}s window (took ${verifierElapsedSeconds(verifierResult)}s); the code is not failing, the suite is slow. Rerun with more room.`;
1728
+ }
1711
1729
  if (verifierResult.passed) {
1712
1730
  if (/^git\s+diff\s+--check\b/i.test(command)) return 'I ran the diff cleanliness check.';
1713
1731
  if (/\bnode\s+--test\b/i.test(command)) return 'I ran the behavior checks.';
@@ -1732,6 +1750,9 @@ function missionVerifierHighLevelTestText(verifierResult, mission) {
1732
1750
  : 'The verify engine did not provide a passing changed-surface verdict.';
1733
1751
  }
1734
1752
  const command = verifierResult.command || mission.verifier || 'configured verifier';
1753
+ if (!verifierResult.passed && verifierResult.timed_out) {
1754
+ return `Verifier command timed out after ${verifierElapsedSeconds(verifierResult)}s against a ${verifierTimeoutSeconds(verifierResult)}s window: ${command}. No pass/fail verdict was reached; the suite outran the window rather than the code failing.`;
1755
+ }
1735
1756
  const outcome = verifierResult.passed ? 'passed' : 'failed';
1736
1757
  if (/^git\s+diff\s+--check\b/i.test(command)) {
1737
1758
  return `Diff cleanliness check ${outcome}: no whitespace or patch-format issues in the changed files.`;
@@ -1964,12 +1985,22 @@ function shouldMissionRunTrustedRoom(rawObjective, args = []) {
1964
1985
  return missionRunTrustedRoomSignals(rawObjective);
1965
1986
  }
1966
1987
 
1988
+ const MISSION_BARE_COMMAND_WORDS = new Set(['go', 'keep', 'going', 'do', 'it', 'start', 'run', 'continue']);
1989
+
1990
+ function missionObjectiveIsJunk(value) {
1991
+ const objective = String(value || '').trim();
1992
+ if (!objective || objective.startsWith('-')) return true;
1993
+ if (/[\u0000-\u001f\u007f-\u009f\ufffd]/u.test(objective)) return true;
1994
+ if (objective.replace(/\s/g, '').length < 8) return true;
1995
+ const words = objective.toLowerCase().match(/[a-z]+/g) || [];
1996
+ return words.length > 0
1997
+ && objective.replace(/[^a-z]+/gi, '').length > 0
1998
+ && words.every((word) => MISSION_BARE_COMMAND_WORDS.has(word));
1999
+ }
2000
+
1967
2001
  function missionRunConcreteTitle(value) {
1968
2002
  const title = String(value || '').replace(/\s+/g, ' ').trim();
1969
- if (!title) return '';
1970
- if (/^(go|go go|go go go|keep going|do it|start|run|continue)$/i.test(title)) return '';
1971
- if (title.length < 8) return '';
1972
- return title;
2003
+ return missionObjectiveIsJunk(title) ? '' : title;
1973
2004
  }
1974
2005
 
1975
2006
  function selectMissionRunUsefulTarget(rawObjective, root = process.cwd()) {
@@ -2207,6 +2238,9 @@ function missionFullBudgetRemainingSeconds(mission, nowMs = Date.now()) {
2207
2238
  }
2208
2239
 
2209
2240
  function missionBudgetContinuationText(mission, nowMs = Date.now()) {
2241
+ // Paused/stopped/blocked missions are not running: never sell remaining time
2242
+ // as a live commitment (CLI-1192: paused overnight loops read as healthy).
2243
+ if (!GOAL_LOOP_STATUSES.has(String(mission?.status || ''))) return null;
2210
2244
  const remaining = missionFullBudgetRemainingSeconds(mission, nowMs);
2211
2245
  if (remaining <= 0) return null;
2212
2246
  const budget = String(mission?.budget_contract?.budget_label || 'promised time').trim();
@@ -2218,10 +2252,15 @@ function missionBudgetContinuationText(mission, nowMs = Date.now()) {
2218
2252
  }
2219
2253
 
2220
2254
  function missionHumanStatusText(mission, nowMs = Date.now()) {
2255
+ const rawStatus = String(mission?.status || 'unknown');
2256
+ if (rawStatus === 'paused') {
2257
+ const reason = String(mission?.stop_reason || mission?.pause_reason || '').trim();
2258
+ return gateForHuman(reason ? `paused: ${reason}` : 'paused').text;
2259
+ }
2221
2260
  const continuation = missionBudgetContinuationText(mission, nowMs);
2222
2261
  const status = continuation
2223
2262
  ? `working for the full ${String(mission?.budget_contract?.budget_label || 'promised time').trim()}`
2224
- : String(mission?.status || 'unknown');
2263
+ : rawStatus;
2225
2264
  return gateForHuman(status).text;
2226
2265
  }
2227
2266
 
@@ -3852,16 +3891,15 @@ function findActiveTwinMission(objective, owner, root = process.cwd()) {
3852
3891
  function startMission(args, options = {}) {
3853
3892
  const asJson = wantsJson(args);
3854
3893
  const firstArg = String(args[0] || '').trim().toLowerCase();
3855
- if (hasFlag(args, '--help') || hasFlag(args, '-h') || firstArg === 'help') {
3894
+ if (args.length === 1 && (firstArg === '--help' || firstArg === '-h' || firstArg === 'help')) {
3856
3895
  console.log('Usage: atris mission start "<objective>" --owner <member> --verify "..." [--destination "..."] [--no-verify] [--always-on] [--budget quick|long|deep] [--runner manual|claude|atris2|codex_goal|auto]');
3857
3896
  console.log('Run `atris mission --help` for the full option list.');
3858
3897
  process.exit(0);
3859
3898
  }
3860
3899
  let mission = applyMissionStartPatch(missionFromArgs(args), options.missionPatch);
3861
- // A flag-looking or empty objective is a typo, not a mission.
3862
3900
  const rawObjective = String(mission.objective || '').trim();
3863
- if (!rawObjective || rawObjective.startsWith('-')) {
3864
- exitMissionError(`mission start needs a quoted objective (got ${rawObjective ? `"${rawObjective}"` : 'nothing'}). Usage: atris mission start "<objective>" --owner <member>`, 1, asJson);
3901
+ if (missionObjectiveIsJunk(rawObjective)) {
3902
+ exitMissionError(`mission start refused junk objective: ${rawObjective ? `"${rawObjective}"` : 'nothing'}`, 1, asJson);
3865
3903
  }
3866
3904
  // Pasting a mission id where an objective goes is an id mismatch, not a new
3867
3905
  // mission: recover the existing record or explain where it actually lives.
@@ -4251,6 +4289,11 @@ async function startMissionFromRunObjective(objective, args) {
4251
4289
 
4252
4290
  function attachMissionTask(args) {
4253
4291
  const asJson = wantsJson(args);
4292
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
4293
+ console.log('Usage: atris mission attach-task <id> [--json]');
4294
+ console.log('Run `atris mission --help` for the full option list.');
4295
+ process.exit(0);
4296
+ }
4254
4297
  const ref = stripKnownFlags(args, [], ['--json'])[0] || '';
4255
4298
  if (!ref) {
4256
4299
  exitMissionError('Usage: atris mission attach-task <id> [--json]', 1, asJson);
@@ -5148,12 +5191,24 @@ function missionReportFor(mission, root = process.cwd()) {
5148
5191
  const timeline = missionReportTimeline(mission, root);
5149
5192
  const verificationDebt = missionVerificationDebt(mission, root);
5150
5193
  const workerCheckedIn = Boolean(explicitWorkerReceipt) || timeline.length > 0;
5194
+ const driver = missionDriverHealth(mission, root);
5195
+ const driverAlive = Boolean(driver && driver.alive);
5151
5196
  const workerReceiptPath = mission.worker_receipt_path || (receipt && verifierReceiptPath) || null;
5152
5197
  const verifierPassed = mission.verifier_result && mission.verifier_result.passed === true;
5153
5198
  const pendingBudgetContinuation = missionBudgetContinuationText(mission);
5154
5199
  const budgetContinuation = workerCheckedIn ? pendingBudgetContinuation : null;
5155
- const operatorOutcome = !workerCheckedIn && pendingBudgetContinuation
5200
+ const awaitingFirstWorker = !workerCheckedIn && Boolean(pendingBudgetContinuation);
5201
+ // An active status implies a worker is running. With no receipt AND no live
5202
+ // driver, that claim is a lie: report that no worker is running and name the
5203
+ // command that starts one instead of saying the mission is in progress.
5204
+ const noLiveWorker = !workerCheckedIn
5205
+ && !driverAlive
5206
+ && !pendingBudgetContinuation
5207
+ && GOAL_LOOP_STATUSES.has(String(mission.status || ''));
5208
+ const operatorOutcome = awaitingFirstWorker
5156
5209
  ? 'No worker has checked in yet.'
5210
+ : noLiveWorker
5211
+ ? 'No worker is running; nothing has produced a receipt yet.'
5157
5212
  : budgetContinuation
5158
5213
  ? 'The last verifier passed; full-budget work is continuing.'
5159
5214
  : mission.operator_outcome
@@ -5162,8 +5217,10 @@ function missionReportFor(mission, root = process.cwd()) {
5162
5217
  id: mission.id,
5163
5218
  objective: mission.objective,
5164
5219
  status: mission.status,
5165
- human_status: !workerCheckedIn && pendingBudgetContinuation
5220
+ human_status: awaitingFirstWorker
5166
5221
  ? 'waiting for the first worker check-in'
5222
+ : noLiveWorker
5223
+ ? 'no live worker (nothing is running)'
5167
5224
  : missionHumanStatusText(mission),
5168
5225
  budget_continuation: budgetContinuation,
5169
5226
  operator_outcome: operatorOutcome,
@@ -5175,9 +5232,10 @@ function missionReportFor(mission, root = process.cwd()) {
5175
5232
  verifier_receipt_path: verifierReceiptPath,
5176
5233
  proof_text: mission.receipt_path ? missionStatusProofText(mission) : null,
5177
5234
  operator_next: budgetContinuation
5178
- || (!workerCheckedIn && pendingBudgetContinuation ? 'Wait for the first worker check-in.' : null)
5235
+ || (awaitingFirstWorker ? 'Wait for the first worker check-in.' : null)
5179
5236
  || mission.operator_next
5180
5237
  || mission.next_action
5238
+ || (noLiveWorker ? `Start a worker: atris mission run ${mission.id}` : null)
5181
5239
  || 'Review the mission state.',
5182
5240
  };
5183
5241
  }
@@ -5857,19 +5915,29 @@ function runVerifier(command, root = process.cwd()) {
5857
5915
  if (!command) return null;
5858
5916
  const resolvedCommand = resolveVerifierCommand(command);
5859
5917
  const { envWithNodeDir } = require('../lib/spawn-env');
5918
+ const timeoutMs = missionVerifierTimeoutMs();
5919
+ const startedAt = Date.now();
5860
5920
  const result = spawnSync(resolvedCommand, {
5861
5921
  cwd: root,
5862
5922
  shell: true,
5863
5923
  encoding: 'utf8',
5864
- timeout: missionVerifierTimeoutMs(),
5924
+ timeout: timeoutMs,
5865
5925
  env: envWithNodeDir(process.env),
5866
5926
  });
5927
+ const elapsedMs = Date.now() - startedAt;
5928
+ // A verifier that blows its window is not a failing check: the code may be
5929
+ // fine and the suite merely slow (or starved by a concurrent flight). Report
5930
+ // it as a distinct timed-out state so operators do not read slow as broken.
5931
+ const timedOut = result.error ? result.error.code === 'ETIMEDOUT' : false;
5867
5932
  return {
5868
5933
  command,
5869
5934
  resolved_command: resolvedCommand === command ? null : resolvedCommand,
5870
5935
  status: result.status,
5871
5936
  signal: result.signal || null,
5872
5937
  passed: result.status === 0,
5938
+ timed_out: timedOut,
5939
+ elapsed_ms: elapsedMs,
5940
+ timeout_ms: timeoutMs,
5873
5941
  stdout: String(result.stdout || '').slice(-4000),
5874
5942
  stderr: String(result.stderr || '').slice(-4000),
5875
5943
  };
@@ -5952,8 +6020,14 @@ function gitWorktreeSnapshot(root = process.cwd()) {
5952
6020
  }
5953
6021
  const entries = String(status.stdout || '').split(/\r?\n/).filter(Boolean).sort();
5954
6022
  const digest = crypto.createHash('sha1').update(entries.join('\n')).digest('hex');
6023
+ const head = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
6024
+ cwd: root,
6025
+ encoding: 'utf8',
6026
+ timeout: 5000,
6027
+ });
5955
6028
  return {
5956
6029
  available: true,
6030
+ head: head.status === 0 ? String(head.stdout || '').trim() : null,
5957
6031
  dirty_count: entries.length,
5958
6032
  dirty_hash: digest,
5959
6033
  dirty_sample: entries.slice(0, 25),
@@ -5961,6 +6035,49 @@ function gitWorktreeSnapshot(root = process.cwd()) {
5961
6035
  };
5962
6036
  }
5963
6037
 
6038
+ function missionProtectedTags(mission, root = process.cwd()) {
6039
+ const tags = [
6040
+ mission?.tag,
6041
+ mission?.lane,
6042
+ ...(Array.isArray(mission?.tags) ? mission.tags : []),
6043
+ ].filter(Boolean);
6044
+ const taskRefs = new Set([
6045
+ mission?.task_id,
6046
+ mission?.task_ref,
6047
+ ...(Array.isArray(mission?.task_ids) ? mission.task_ids : []),
6048
+ ].filter(Boolean).map(String));
6049
+ if (!taskRefs.size) return tags;
6050
+ try {
6051
+ const projection = JSON.parse(fs.readFileSync(path.join(root, '.atris', 'state', 'tasks.projection.json'), 'utf8'));
6052
+ for (const task of Array.isArray(projection?.tasks) ? projection.tasks : []) {
6053
+ if (!task || ![task.id, task.display_id, task.legacy_ref].some((ref) => taskRefs.has(String(ref || '')))) continue;
6054
+ if (task.tag) tags.push(task.tag);
6055
+ if (Array.isArray(task.tags)) tags.push(...task.tags);
6056
+ }
6057
+ } catch {}
6058
+ return tags;
6059
+ }
6060
+
6061
+ function inspectMissionTickProtectedDiff(mission, before, root = process.cwd()) {
6062
+ const after = gitWorktreeSnapshot(root);
6063
+ if (before?.reason === 'not-git-worktree' && after?.reason === 'not-git-worktree') {
6064
+ return {
6065
+ ok: true,
6066
+ allowed: true,
6067
+ status: 'clear',
6068
+ reason: 'no git worktree exists, so this tick cannot commit or land',
6069
+ unreadable: false,
6070
+ surfaces: [],
6071
+ matches: [],
6072
+ };
6073
+ }
6074
+ return inspectMissionProtectedDiff({
6075
+ root,
6076
+ baseRef: before?.head || '',
6077
+ tags: missionProtectedTags(mission, root),
6078
+ });
6079
+ }
6080
+
5964
6081
  // Porcelain v1 entries are "XY path" or "XY old -> new"; baselines compare by
5965
6082
  // post-rename path so a status-letter change alone never counts as new dirt.
5966
6083
  function porcelainEntryPath(line) {
@@ -6264,7 +6381,14 @@ function missionSortTime(mission) {
6264
6381
  }
6265
6382
 
6266
6383
  function selectDueMission(root = process.cwd(), now = new Date(), options = {}) {
6267
- const candidates = listMissions(root)
6384
+ const missions = listMissions(root);
6385
+ for (const mission of missions) {
6386
+ if (!missionIsRunnable(mission) || !missionObjectiveIsJunk(mission.objective)) continue;
6387
+ stopMissionRecord(mission, 'junk_objective_gate', root, { writeStopReceipt: false });
6388
+ console.error(`auto-stopped junk mission: "${String(mission.objective || '')}"`);
6389
+ }
6390
+ const candidates = missions
6391
+ .filter((mission) => !missionObjectiveIsJunk(mission.objective))
6268
6392
  .filter((mission) => missionSelectableForLoop(mission, now))
6269
6393
  .filter((mission) => !options.headlessOnly || !runnerUsesCallerSession(mission.runner))
6270
6394
  .filter((mission) => !missionTaskHumanAcceptWaiting(mission))
@@ -7876,6 +8000,14 @@ function spawnMissionDriver(mission, root = process.cwd()) {
7876
8000
  log_path: logPath,
7877
8001
  }, null, 2) + '\n', 'utf8');
7878
8002
  child.unref();
8003
+ // A returned pid is not proof the engine is running: spawn can hand back a
8004
+ // pid for a child that never execs or exits immediately. Verify the process
8005
+ // is actually alive before claiming a live worker, and drop the misleading
8006
+ // driver-state file when it is not.
8007
+ if (!child.pid || !missionLockOwnerIsAlive(child.pid)) {
8008
+ try { fs.rmSync(paths.stateFile, { force: true }); } catch {}
8009
+ return { ok: false, error: 'mission driver exited before it could start', pid: child.pid || null };
8010
+ }
7879
8011
  return {
7880
8012
  ok: true,
7881
8013
  pid: child.pid,
@@ -7949,7 +8081,13 @@ function detachMissionRun(args, mission, root = process.cwd(), asJson = false) {
7949
8081
 
7950
8082
  function probeClaudeBinary() {
7951
8083
  const runnerBin = resolveClaudeRunnerBin();
7952
- if (resolveClaudeRunnerCommandTemplate()) {
8084
+ const commandTemplate = resolveClaudeRunnerCommandTemplate();
8085
+ if (commandTemplate) {
8086
+ // A command template fully replaces the claude invocation (GLM/OpenAI/
8087
+ // local runners). The runner bin only matters when the template actually
8088
+ // references {bin}; requiring `claude` on PATH otherwise defeats the
8089
+ // template's purpose and breaks generic runners on claude-less machines.
8090
+ if (!commandTemplate.includes('{bin}')) return { ok: true };
7953
8091
  const probe = spawnSync('sh', ['-c', `command -v ${shellQuote(runnerBin)}`], { encoding: 'utf8', timeout: 8000 });
7954
8092
  if (probe.status !== 0) return { ok: false, error: `${runnerBin} CLI not found` };
7955
8093
  return { ok: true };
@@ -8072,6 +8210,9 @@ function detectUnavailableModel(text) {
8072
8210
  // need a resume; a model-unavailable pause is a config error a bare resume won't fix,
8073
8211
  // so name the dead id and the two knobs that change it.
8074
8212
  function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastErrorReason = null) {
8213
+ if (pauseReason === 'protected-lane-review') {
8214
+ return `review the protected diff and its receipt before resuming: atris mission run ${missionId}`;
8215
+ }
8075
8216
  if (pauseReason === 'too-deep') {
8076
8217
  return 'stopped: helpers were spawning helpers too many levels deep';
8077
8218
  }
@@ -8260,13 +8401,31 @@ function spawnGenericRunnerTick(mission, opts) {
8260
8401
  resolve({ ok: false, error: e.message, sessionIds: [], aborted: false, timedOut: false, authExpired: false, brief_id: briefId });
8261
8402
  return;
8262
8403
  }
8404
+ let gitGuard;
8405
+ try {
8406
+ gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
8407
+ } catch (error) {
8408
+ resolve({
8409
+ ok: false,
8410
+ reason: 'protected-diff-unreadable',
8411
+ error: error.message || String(error),
8412
+ protected_lane_guard: unreadableMissionGuard(error),
8413
+ sessionIds: [],
8414
+ aborted: false,
8415
+ timedOut: false,
8416
+ authExpired: false,
8417
+ brief_id: briefId,
8418
+ });
8419
+ return;
8420
+ }
8263
8421
 
8264
8422
  const startedAt = Date.now();
8265
- const proc = spawn('sh', ['-lc', cmd], {
8423
+ const guardedCommand = `PATH=${shellQuote(gitGuard.pathPrefix)}:$PATH; export PATH; ${cmd}`;
8424
+ const proc = spawn('sh', ['-lc', guardedCommand], {
8266
8425
  cwd,
8267
8426
  stdio: ['ignore', 'pipe', 'pipe'],
8268
8427
  detached: true,
8269
- env: { ...process.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
8428
+ env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
8270
8429
  });
8271
8430
  updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
8272
8431
  let stdout = '';
@@ -8297,6 +8456,7 @@ function spawnGenericRunnerTick(mission, opts) {
8297
8456
  proc.on('close', (code) => {
8298
8457
  clearTimeout(timer);
8299
8458
  cleanupPrompt();
8459
+ gitGuard.cleanup();
8300
8460
  if (signal) signal.removeEventListener?.('abort', onAbort);
8301
8461
  updateMissionLockOwner(missionLock, missionLock?.driverPid);
8302
8462
  const finalText = String(stdout || '').trim();
@@ -8325,6 +8485,7 @@ function spawnGenericRunnerTick(mission, opts) {
8325
8485
  proc.on('error', (e) => {
8326
8486
  clearTimeout(timer);
8327
8487
  cleanupPrompt();
8488
+ gitGuard.cleanup();
8328
8489
  if (signal) signal.removeEventListener?.('abort', onAbort);
8329
8490
  updateMissionLockOwner(missionLock, missionLock?.driverPid);
8330
8491
  resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false, brief_id: briefId });
@@ -8373,6 +8534,22 @@ function spawnClaudeTick(mission, opts) {
8373
8534
  const dispatchDepth = missionDispatchDepth();
8374
8535
  if (dispatchDepth.tooDeep) return Promise.resolve(tooDeepRunnerResult(dispatchDepth));
8375
8536
  return new Promise((resolve) => {
8537
+ let gitGuard;
8538
+ try {
8539
+ gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
8540
+ } catch (error) {
8541
+ resolve({
8542
+ ok: false,
8543
+ reason: 'protected-diff-unreadable',
8544
+ error: error.message || String(error),
8545
+ protected_lane_guard: unreadableMissionGuard(error),
8546
+ sessionIds: [],
8547
+ aborted: false,
8548
+ timedOut: false,
8549
+ authExpired: false,
8550
+ });
8551
+ return;
8552
+ }
8376
8553
  const args = [
8377
8554
  '-p', prompt,
8378
8555
  '--output-format', 'stream-json',
@@ -8392,7 +8569,7 @@ function spawnClaudeTick(mission, opts) {
8392
8569
  cwd,
8393
8570
  stdio: ['ignore', 'pipe', 'pipe'],
8394
8571
  detached: true,
8395
- env: { ...process.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
8572
+ env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
8396
8573
  });
8397
8574
  updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
8398
8575
 
@@ -8455,6 +8632,7 @@ function spawnClaudeTick(mission, opts) {
8455
8632
 
8456
8633
  proc.on('close', (code) => {
8457
8634
  clearTimeout(timer);
8635
+ gitGuard.cleanup();
8458
8636
  if (signal) signal.removeEventListener?.('abort', onAbort);
8459
8637
  updateMissionLockOwner(missionLock, missionLock?.driverPid);
8460
8638
  const ok = code === 0 && !isError && !timedOut && !aborted;
@@ -8484,6 +8662,7 @@ function spawnClaudeTick(mission, opts) {
8484
8662
 
8485
8663
  proc.on('error', (e) => {
8486
8664
  clearTimeout(timer);
8665
+ gitGuard.cleanup();
8487
8666
  updateMissionLockOwner(missionLock, missionLock?.driverPid);
8488
8667
  resolve({ ok: false, error: e.message, sessionIds: [], aborted, timedOut, authExpired: false });
8489
8668
  });
@@ -8826,6 +9005,10 @@ async function runMission(args) {
8826
9005
  const startedAt = Date.now();
8827
9006
  let backoffAttempt = 0;
8828
9007
  let lastRateLimit = null;
9008
+ // A usage-wall cooldown belongs to the engine that hit it, not the whole run.
9009
+ // Tracking which engine set it lets an auto-runner swap to another ready engine
9010
+ // and keep working instead of waiting the wall out (or pausing for a human).
9011
+ let rateLimitedEngineId = null;
8829
9012
  let continuationGoal = null;
8830
9013
 
8831
9014
  const sessionLabel = skipWorker
@@ -8885,6 +9068,18 @@ async function runMission(args) {
8885
9068
  result.engine_fallback_reason = tickSelection.engine_fallback_reason;
8886
9069
  }
8887
9070
 
9071
+ // Usage-wall engine swap: when a prior tick's engine hit a rate-limit wall and
9072
+ // the auto runner has already resolved this tick to a *different* ready engine
9073
+ // (the walled one is now credit_out and excluded from resolution), the run-wide
9074
+ // cooldown no longer applies. Clear it so the fresh engine runs now instead of
9075
+ // the loop sleeping through — or pausing on `rate-limit-exceeded-wall` for — a
9076
+ // wall this engine never hit.
9077
+ if (lastRateLimit && rateLimitedEngineId && tickEngineId && tickEngineId !== rateLimitedEngineId) {
9078
+ result.engine_swapped_from = rateLimitedEngineId;
9079
+ lastRateLimit = null;
9080
+ rateLimitedEngineId = null;
9081
+ }
9082
+
8888
9083
  // Active-hours gate
8889
9084
  if (engineBackedTick && !tickEngineId) {
8890
9085
  result = { ...result, status: 'errored', reason: 'no-ready-engine' };
@@ -8916,55 +9111,76 @@ async function runMission(args) {
8916
9111
  drill: drillResult,
8917
9112
  };
8918
9113
  } else if (tickAtris2Runner) {
8919
- const pingDrain = consumeMissionPings(mission, cwd);
8920
- mission = pingDrain.mission;
8921
- runtimeMission = runtimeView(mission);
8922
- const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
8923
- const { runAtris2Turn } = require('./probe');
8924
- const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
8925
- const tickController = new AbortController();
8926
- let wallExpired = false;
8927
- const abortTick = () => tickController.abort();
8928
- controller.signal.addEventListener('abort', abortTick, { once: true });
8929
- const tickTimer = setTimeout(() => {
8930
- wallExpired = true;
8931
- tickController.abort();
8932
- }, Math.max(1, Math.floor(remainingWall * 1000)));
8933
- let turn;
9114
+ let atris2GitGuard = null;
8934
9115
  try {
8935
- turn = await runAtris2Turn({
8936
- prompt,
8937
- model: tickRuntimeMission.model || 'atris:fast',
8938
- business: businessId,
8939
- maxTurns: 16,
8940
- signal: tickController.signal,
8941
- // CLI-231: mission ticks execute relayed ops in the mission's own
8942
- // workspace, not the hosted ai-computer filesystem.
8943
- localCwd: cwd,
8944
- });
8945
- } finally {
8946
- clearTimeout(tickTimer);
8947
- controller.signal.removeEventListener('abort', abortTick);
9116
+ atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
9117
+ } catch (error) {
9118
+ result = {
9119
+ ...result,
9120
+ status: 'ran',
9121
+ reason: 'protected-diff-unreadable',
9122
+ ran: true,
9123
+ atris2: {
9124
+ ok: false,
9125
+ error: error.message || String(error),
9126
+ protected_lane_guard: unreadableMissionGuard(error),
9127
+ },
9128
+ };
8948
9129
  }
8949
- result.atris2 = {
8950
- ok: turn.ok,
8951
- engine: turn.engine,
8952
- model: tickRuntimeMission.model || 'atris:fast',
8953
- tools_run: turn.tools_run,
8954
- unsupported: turn.unsupported,
8955
- duration_ms: turn.duration_ms,
8956
- error: turn.error,
8957
- backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
8958
- receipt_text: String(turn.text || '').slice(0, 4000),
8959
- };
8960
- if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
8961
- if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
8962
- if (wallExpired) {
8963
- result = { ...result, status: 'errored', reason: 'wall-exceeded-during-tick' };
8964
- } else if (!turn.ok || !String(turn.text || '').trim()) {
8965
- result = { ...result, status: 'errored', reason: atris2TurnErrorReason(turn.error) };
8966
- } else {
8967
- result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
9130
+ if (atris2GitGuard) {
9131
+ const pingDrain = consumeMissionPings(mission, cwd);
9132
+ mission = pingDrain.mission;
9133
+ runtimeMission = runtimeView(mission);
9134
+ const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pingDrain.pings);
9135
+ const { runAtris2Turn } = require('./probe');
9136
+ const businessId = businessIdForAtris2Mission(tickRuntimeMission, cwd);
9137
+ const tickController = new AbortController();
9138
+ let wallExpired = false;
9139
+ const abortTick = () => tickController.abort();
9140
+ controller.signal.addEventListener('abort', abortTick, { once: true });
9141
+ const tickTimer = setTimeout(() => {
9142
+ wallExpired = true;
9143
+ tickController.abort();
9144
+ }, Math.max(1, Math.floor(remainingWall * 1000)));
9145
+ let turn;
9146
+ try {
9147
+ turn = await runAtris2Turn({
9148
+ prompt,
9149
+ model: tickRuntimeMission.model || 'atris:fast',
9150
+ business: businessId,
9151
+ maxTurns: 16,
9152
+ signal: tickController.signal,
9153
+ // CLI-231: mission ticks execute relayed ops in the mission's own
9154
+ // workspace, not the hosted ai-computer filesystem.
9155
+ localCwd: cwd,
9156
+ localEnv: atris2GitGuard.env,
9157
+ localPathPrefix: atris2GitGuard.pathPrefix,
9158
+ });
9159
+ } finally {
9160
+ atris2GitGuard.cleanup();
9161
+ clearTimeout(tickTimer);
9162
+ controller.signal.removeEventListener('abort', abortTick);
9163
+ }
9164
+ result.atris2 = {
9165
+ ok: turn.ok,
9166
+ engine: turn.engine,
9167
+ model: tickRuntimeMission.model || 'atris:fast',
9168
+ tools_run: turn.tools_run,
9169
+ unsupported: turn.unsupported,
9170
+ duration_ms: turn.duration_ms,
9171
+ error: turn.error,
9172
+ backend_unavailable: isTransientAtris2BackendError(turn.error) || undefined,
9173
+ receipt_text: String(turn.text || '').slice(0, 4000),
9174
+ };
9175
+ if (controller.signal.aborted) { pauseReason = 'aborted-during-atris2'; break; }
9176
+ if (turn.error === 'not-logged-in') { pauseReason = 'auth-required'; break; }
9177
+ if (wallExpired) {
9178
+ result = { ...result, status: 'errored', reason: 'wall-exceeded-during-tick' };
9179
+ } else if (!turn.ok || !String(turn.text || '').trim()) {
9180
+ result = { ...result, status: 'errored', reason: atris2TurnErrorReason(turn.error) };
9181
+ } else {
9182
+ result = { ...result, status: 'ran', reason: 'tick-ok', ran: true };
9183
+ }
8968
9184
  }
8969
9185
  } else {
8970
9186
  let sessionMode = sessionId ? 'resume' : 'set';
@@ -9006,6 +9222,7 @@ async function runMission(args) {
9006
9222
  result.claude = {
9007
9223
  ok: claudeResult.ok,
9008
9224
  reason: claudeResult.reason,
9225
+ protected_lane_guard: claudeResult.protected_lane_guard || null,
9009
9226
  brief_id: claudeResult.brief_id || null,
9010
9227
  summary: claudeResult.summary,
9011
9228
  receipt_text: claudeResult.receipt_text,
@@ -9026,6 +9243,7 @@ async function runMission(args) {
9026
9243
  // treating an allowed resetsAt as one pauses every timed run after
9027
9244
  // tick 1 with rate-limit-exceeded-wall.
9028
9245
  lastRateLimit = claudeResult.rate_limit_info.status === 'allowed' ? null : claudeResult.rate_limit_info;
9246
+ rateLimitedEngineId = lastRateLimit ? tickEngineId : null;
9029
9247
  }
9030
9248
  if (claudeResult.aborted) { pauseReason = 'aborted-during-claude'; break; }
9031
9249
  if (claudeResult.authExpired) { pauseReason = 'auth-required'; break; }
@@ -9091,6 +9309,19 @@ async function runMission(args) {
9091
9309
  // and caller-session ticks must not launch it behind the operator's
9092
9310
  // back. Leaving verifier_passed unset also lets the idle-stop breaker
9093
9311
  // judge these ticks from the worktree signal instead of hanging here.
9312
+ const runnerGuard = result.claude?.protected_lane_guard || result.atris2?.protected_lane_guard || null;
9313
+ if (result.status === 'ran' || runnerGuard) {
9314
+ const protectedLaneGuard = runnerGuard?.allowed === false
9315
+ ? runnerGuard
9316
+ : inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
9317
+ result.protected_lane_guard = protectedLaneGuard;
9318
+ if (!protectedLaneGuard.allowed) {
9319
+ result.status = 'paused-for-review';
9320
+ result.reason = 'protected-lane-review';
9321
+ result.ran = false;
9322
+ pauseReason = 'protected-lane-review';
9323
+ }
9324
+ }
9094
9325
  let verifierResult = null;
9095
9326
  let receiptPath = null;
9096
9327
  if (result.status === 'ran' && verifyEach) {
@@ -9246,6 +9477,7 @@ async function runMission(args) {
9246
9477
  break;
9247
9478
  }
9248
9479
 
9480
+ if (pauseReason === 'protected-lane-review') break;
9249
9481
  if (callerSessionRunner && result.status === 'ran') break;
9250
9482
  if (newStatus === 'complete' || (newStatus === 'ready' && !mission.always_on && !fullBudgetMode)) break;
9251
9483
  // BCK-1324: two consecutive ticks that each self-report "ran" but leave no
@@ -9516,6 +9748,7 @@ function tickMission(args) {
9516
9748
  const tickIdx = lastTickIndex + 1;
9517
9749
  const tickWorktreeBefore = gitWorktreeSnapshot(cwd);
9518
9750
  const worktreeBaseline = loadMissionWorktreeBaseline(mission.id, cwd);
9751
+ const protectedLaneGuard = inspectMissionTickProtectedDiff(mission, tickWorktreeBefore, cwd);
9519
9752
 
9520
9753
  const effectiveVerifier = effectiveMissionVerifier(mission);
9521
9754
  const verifierCommand = verify
@@ -9524,7 +9757,7 @@ function tickMission(args) {
9524
9757
  if (verifierCommand) assertMissionVerifier(verifierCommand, asJson);
9525
9758
 
9526
9759
  let verifierResult = null;
9527
- if (verify && verifierCommand) {
9760
+ if (protectedLaneGuard.allowed && verify && verifierCommand) {
9528
9761
  verifierResult = runVerifier(verifierCommand);
9529
9762
  }
9530
9763
  const tickWorktree = worktreeReceipt(tickWorktreeBefore, gitWorktreeSnapshot(cwd), { verifier: verifierCommand || effectiveVerifier, baseline: worktreeBaseline });
@@ -9533,16 +9766,17 @@ function tickMission(args) {
9533
9766
  // receipt text in --summary.
9534
9767
  const layerInfo = extractLayerFromReceiptText(summary || '', tickWorktree?.new_since_baseline_sample);
9535
9768
  const tickRecord = {
9536
- status: 'ran',
9537
- reason: 'tick-recorded',
9769
+ status: protectedLaneGuard.allowed ? 'ran' : 'paused-for-review',
9770
+ reason: protectedLaneGuard.allowed ? 'tick-recorded' : 'protected-lane-review',
9538
9771
  tick_index: tickIdx,
9539
- ran: true,
9772
+ ran: protectedLaneGuard.allowed,
9540
9773
  started_at: tickStart,
9541
9774
  claude: { skipped: true, reason: 'orchestrator-is-caller-session' },
9542
9775
  summary: summary || null,
9543
9776
  layer: layerInfo.layer,
9544
9777
  layer_source: layerInfo.source,
9545
9778
  verifier_passed: verifierResult ? !!verifierResult.passed : null,
9779
+ protected_lane_guard: protectedLaneGuard,
9546
9780
  finished_at: stampIso(),
9547
9781
  worktree: tickWorktree,
9548
9782
  };
@@ -9555,6 +9789,7 @@ function tickMission(args) {
9555
9789
  started_at: tickStart,
9556
9790
  },
9557
9791
  verifier_result: verifierResult,
9792
+ protected_lane_guard: protectedLaneGuard,
9558
9793
  rate_limit_info: null,
9559
9794
  worktree: tickWorktree,
9560
9795
  });
@@ -9566,7 +9801,10 @@ function tickMission(args) {
9566
9801
  ? nextCandidateTickAction(mission)
9567
9802
  : 'attach task, verifier, or proof');
9568
9803
  const nextGoalChain = advanceMissionGoalChain(mission.goal_chain, summary, verifierResult);
9569
- if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
9804
+ if (!protectedLaneGuard.allowed) {
9805
+ status = 'paused';
9806
+ nextAction = `review the protected diff and receipt before resuming: atris mission run ${mission.id}`;
9807
+ } else if (verifierResult?.passed && nextGoalChain && !nextGoalChain.pause_ready) {
9570
9808
  status = 'running';
9571
9809
  nextAction = missionGoalChainNextAction(nextGoalChain);
9572
9810
  } else if (verifierResult?.passed) {
@@ -9591,10 +9829,10 @@ function tickMission(args) {
9591
9829
  }
9592
9830
  const clearsPauseState = !['paused', 'stopped'].includes(status);
9593
9831
  const nextMission = {
9594
- ...mission,
9595
- status,
9596
- paused_at: clearsPauseState ? null : mission.paused_at || null,
9597
- stop_reason: clearsPauseState ? null : mission.stop_reason || null,
9832
+ ...mission,
9833
+ status,
9834
+ paused_at: clearsPauseState ? null : tickRecord.finished_at,
9835
+ stop_reason: clearsPauseState ? null : tickRecord.reason,
9598
9836
  resumed_at: clearsPauseState && mission.status === 'paused' ? tickRecord.finished_at : mission.resumed_at || null,
9599
9837
  receipt_path: receiptPath,
9600
9838
  last_tick_at: tickRecord.finished_at,
@@ -9698,6 +9936,11 @@ function missionCompletionGate(mission, proof, root = process.cwd()) {
9698
9936
 
9699
9937
  function completeMission(args) {
9700
9938
  const asJson = wantsJson(args);
9939
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
9940
+ console.log('Usage: atris mission complete <id> --proof "..."');
9941
+ console.log('Run `atris mission --help` for the full option list.');
9942
+ process.exit(0);
9943
+ }
9701
9944
  const force = hasFlag(args, '--force');
9702
9945
  const proof = readFlag(args, '--proof', '');
9703
9946
  const ref = stripKnownFlags(args, ['--proof'], ['--json', '--force'])[0] || '';
@@ -9773,31 +10016,21 @@ function completeMission(args) {
9773
10016
  );
9774
10017
  }
9775
10018
 
9776
- function stopMission(args) {
9777
- const asJson = wantsJson(args);
9778
- const reason = readFlag(args, '--reason', 'stopped by operator');
9779
- const pause = hasFlag(args, '--pause');
9780
- const ref = stripKnownFlags(args, ['--reason'], ['--json', '--pause'])[0] || '';
9781
- if (!ref) {
9782
- exitMissionError('Usage: atris mission stop <id> [--pause] [--reason "..."]', 1, asJson);
9783
- }
9784
- const mission = resolveMission(ref);
9785
- if (!mission) {
9786
- exitMissingMission(ref, 1, asJson);
9787
- }
10019
+ function stopMissionRecord(mission, reason, root = process.cwd(), options = {}) {
10020
+ const pause = options.pause === true;
9788
10021
  const status = pause ? 'paused' : 'stopped';
9789
10022
  // Full stops abandon work, so leave evidence: snapshot the worktree against
9790
10023
  // the mission baseline (what did this mission leave dirty?) before pruning it.
9791
10024
  let receiptPath = null;
9792
- if (!pause) {
9793
- const snapshot = gitWorktreeSnapshot(process.cwd());
10025
+ if (!pause && options.writeStopReceipt !== false) {
10026
+ const snapshot = gitWorktreeSnapshot(root);
9794
10027
  const worktree = worktreeReceipt(snapshot, snapshot, {
9795
10028
  verifier: mission.verifier,
9796
- baseline: loadMissionWorktreeBaseline(mission.id, process.cwd()),
10029
+ baseline: loadMissionWorktreeBaseline(mission.id, root),
9797
10030
  });
9798
- receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree });
10031
+ receiptPath = writeReceipt(mission, { kind: 'mission_stop', reason, worktree }, root);
9799
10032
  }
9800
- const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission, process.cwd());
10033
+ const baselineSummary = pause ? null : pruneMissionWorktreeBaseline(mission, root);
9801
10034
  const next = {
9802
10035
  ...mission,
9803
10036
  status,
@@ -9808,9 +10041,32 @@ function stopMission(args) {
9808
10041
  worktree_baseline: baselineSummary || mission.worktree_baseline || null,
9809
10042
  next_action: status === 'paused' ? `resume with: atris mission tick ${mission.id}` : 'mission stopped',
9810
10043
  };
9811
- const { mission: saved } = saveMission(next, process.cwd(), pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
9812
- const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id, process.cwd());
9813
- const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason });
10044
+ const { mission: saved } = saveMission(next, root, pause ? 'mission_paused' : 'mission_stopped', { reason, receipt_path: receiptPath });
10045
+ const directGoalRequestCleared = pause ? false : clearDirectRunCodexGoalRequestForMission(saved.id, root);
10046
+ const logPath = appendMemberLog(saved.owner, pause ? 'Mission paused' : 'Mission stopped', { mission: saved.objective, reason }, root);
10047
+ return { saved, receiptPath, directGoalRequestCleared, logPath };
10048
+ }
10049
+
10050
+ function stopMission(args) {
10051
+ const asJson = wantsJson(args);
10052
+ // Help is read-only: `mission stop <id> --help` must never resolve the id and
10053
+ // stop the mission just to be asked for usage.
10054
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
10055
+ console.log('Usage: atris mission stop <id> [--pause] [--reason "..."]');
10056
+ console.log('Run `atris mission --help` for the full option list.');
10057
+ process.exit(0);
10058
+ }
10059
+ const reason = readFlag(args, '--reason', 'stopped by operator');
10060
+ const pause = hasFlag(args, '--pause');
10061
+ const ref = stripKnownFlags(args, ['--reason'], ['--json', '--pause'])[0] || '';
10062
+ if (!ref) {
10063
+ exitMissionError('Usage: atris mission stop <id> [--pause] [--reason "..."]', 1, asJson);
10064
+ }
10065
+ const mission = resolveMission(ref);
10066
+ if (!mission) {
10067
+ exitMissingMission(ref, 1, asJson);
10068
+ }
10069
+ const { saved, receiptPath, directGoalRequestCleared, logPath } = stopMissionRecord(mission, reason, process.cwd(), { pause });
9814
10070
  printJsonOrText(
9815
10071
  { ok: true, action: pause ? 'mission_paused' : 'mission_stopped', mission: saved, receipt_path: receiptPath, log_path: logPath, direct_goal_request_cleared: directGoalRequestCleared },
9816
10072
  [
@@ -10506,6 +10762,11 @@ function inspectMission(args) {
10506
10762
  // output so callers (member ping) can compose their own multi-lane report.
10507
10763
  function pingMission(args, opts = {}) {
10508
10764
  const asJson = args.includes('--json');
10765
+ if (!opts.silent && (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help')) {
10766
+ console.log('Usage: atris mission ping <id> "<note>" [--from <who>] [--json]');
10767
+ console.log('Run `atris mission --help` for the full option list.');
10768
+ process.exit(0);
10769
+ }
10509
10770
  const rest = args.filter((a) => a !== '--json');
10510
10771
  let from = process.env.USER || 'operator';
10511
10772
  const fromIdx = rest.indexOf('--from');
@@ -10674,6 +10935,7 @@ module.exports = {
10674
10935
  extractCheckFeedback,
10675
10936
  loadMissionMap,
10676
10937
  renderMissionStatus,
10938
+ missionObjectiveIsJunk,
10677
10939
  selectDueMission,
10678
10940
  selectAtrisGoalMission,
10679
10941
  selectCodexGoalMission,
@@ -10682,29 +10944,22 @@ module.exports = {
10682
10944
  cappedClaudeReceiptText,
10683
10945
  extractLayerFromReceiptText,
10684
10946
  classifyPathsByLayer,
10685
- collectMissionDoctorFindings,
10686
10947
  resolveClaudeRunnerModel,
10687
10948
  resolveClaudeRunnerBin,
10688
10949
  businessIdForAtris2Mission,
10689
10950
  detectUnavailableModel,
10690
10951
  missionPauseNextAction,
10691
- HUMAN_BLOCKING_PAUSE_REASONS,
10692
10952
  composeHumanBlockingPauseMessage,
10693
10953
  escalateHumanBlockingPause,
10694
- humanBlockingPauseReason,
10695
10954
  consecutiveSameReasonErrors,
10696
10955
  missionVerifierTimeoutMs,
10697
10956
  missionLandingStepSummary,
10698
10957
  missionLandingLines,
10699
10958
  missionVerifierCheckedText,
10700
10959
  missionVerifierHighLevelTestText,
10701
- missionTickVerification,
10702
- missionVerificationDebt,
10703
- missionVerificationDebtLine,
10704
- missionReportFor,
10705
- renderMemberNowMarkdown,
10706
10960
  buildEngineVerifyPrompt,
10707
10961
  engineVerifierResultFromRun,
10962
+ runVerifier,
10708
10963
  missionFullBudgetRemainingSeconds,
10709
10964
  missionBudgetContinuationText,
10710
10965
  missionHumanStatusText,
@@ -10718,4 +10973,6 @@ module.exports = {
10718
10973
  missionJudgmentCard,
10719
10974
  missionShippedCard,
10720
10975
  appendMissionJudgmentCard,
10976
+ acquireMissionLock,
10977
+ releaseMissionLock,
10721
10978
  };